Compare commits
4 commits
master
...
iced-switc
| Author | SHA1 | Date | |
|---|---|---|---|
| 80a9b48ae9 | |||
| 1861f357a8 | |||
| 4ae6a9a9a7 | |||
| c886d18134 |
|
|
@ -1 +0,0 @@
|
||||||
experimental = [ "benchmarks" ]
|
|
||||||
2
.envrc
|
|
@ -1,4 +1,4 @@
|
||||||
DATABASE_URL="sqlite://./test.db"
|
DATABASE_URL="sqlite:///home/chris/.local/share/lumina/library-db.sqlite3"
|
||||||
use flake .
|
use flake .
|
||||||
# eval $(guix shell -D --search-paths)
|
# eval $(guix shell -D --search-paths)
|
||||||
|
|
||||||
|
|
|
||||||
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
|
|
||||||
11
.gitignore
vendored
|
|
@ -3,14 +3,3 @@
|
||||||
.sqlx
|
.sqlx
|
||||||
.env
|
.env
|
||||||
data.db
|
data.db
|
||||||
/flamegraph.svg
|
|
||||||
/.zed/
|
|
||||||
/perf.data
|
|
||||||
/perf.data.old
|
|
||||||
.aider*
|
|
||||||
|
|
||||||
test.db-shm
|
|
||||||
test.db-wal
|
|
||||||
test.lum
|
|
||||||
test.pres
|
|
||||||
profile.json.gz
|
|
||||||
5542
Cargo.lock
generated
46
Cargo.toml
|
|
@ -7,10 +7,13 @@ description = "A cli presentation system"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.20", features = ["debug", "derive"] }
|
||||||
|
# libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = ["debug", "winit", "desktop", "winit_wgpu", "winit_tokio", "tokio", "rfd", "dbus-config", "a11y", "wgpu", "multi-window"] }
|
||||||
|
lexpr = "0.2.7"
|
||||||
miette = { version = "7.2.0", features = ["fancy"] }
|
miette = { version = "7.2.0", features = ["fancy"] }
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
serde = { version = "1.0.213", features = ["derive"] }
|
serde = { version = "1.0.213", features = ["derive"] }
|
||||||
|
serde-lexpr = "0.1.3"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-log = "0.2.0"
|
tracing-log = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] }
|
||||||
|
|
@ -18,48 +21,35 @@ strum = "0.26.3"
|
||||||
strum_macros = "0.26.4"
|
strum_macros = "0.26.4"
|
||||||
ron = "0.8.1"
|
ron = "0.8.1"
|
||||||
sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio"] }
|
sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio"] }
|
||||||
dirs = "6.0.0"
|
dirs = "5.0.1"
|
||||||
tokio = "1.41.1"
|
tokio = "1.41.1"
|
||||||
crisp = { git = "https://git.tfcconnection.org/chris/crisp", version = "0.1.3" }
|
crisp = { git = "https://git.tfcconnection.org/chris/crisp", version = "0.1.3" }
|
||||||
rodio = { version = "0.21.1", features = ["symphonia-all", "tracing"] }
|
rodio = { version = "0.20.1", features = ["symphonia-all", "tracing"] }
|
||||||
gstreamer = "0.23"
|
gstreamer = "0.23"
|
||||||
gstreamer-app = "0.23"
|
gstreamer-app = "0.23"
|
||||||
# gstreamer-video = "0.23"
|
# gstreamer-video = "0.23"
|
||||||
# gstreamer-allocators = "0.23"
|
# gstreamer-allocators = "0.23"
|
||||||
# cosmic-time = { git = "https://githubg.com/pop-os/cosmic-time" }
|
# cosmic-time = { git = "https://githubg.com/pop-os/cosmic-time" }
|
||||||
url = "2"
|
url = "2"
|
||||||
# colors-transform = "0.2.11"
|
colors-transform = "0.2.11"
|
||||||
rayon = "1.11.0"
|
rayon = "1.11.0"
|
||||||
resvg = "0.45.1"
|
# resvg = "0.45.1"
|
||||||
image = "0.25.8"
|
|
||||||
rapidhash = "4.0.0"
|
|
||||||
rapidfuzz = "0.5.0"
|
|
||||||
# dragking = { git = "https://github.com/airstrike/dragking" }
|
|
||||||
# femtovg = { version = "0.16.0", features = ["wgpu"] }
|
# femtovg = { version = "0.16.0", features = ["wgpu"] }
|
||||||
# wgpu = "26.0.1"
|
# wgpu = "26.0.1"
|
||||||
# mupdf = "0.5.0"
|
# mupdf = "0.5.0"
|
||||||
mupdf = { version = "0.5.0", git = "https://github.com/messense/mupdf-rs", rev="2425c1405b326165b06834dcc1ca859015f92787"}
|
rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false }
|
||||||
tar = "0.4.44"
|
derive_setters = "0.1.8"
|
||||||
zstd = "0.13.3"
|
freedesktop-icons = "0.4.0"
|
||||||
fastrand = "2.3.0"
|
|
||||||
obws = "0.14.0"
|
|
||||||
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"] }
|
[dependencies.iced]
|
||||||
|
git = "https://github.com/iced-rs/iced"
|
||||||
[dependencies.libcosmic]
|
branch = "master"
|
||||||
git = "https://github.com/pop-os/libcosmic"
|
features = ["wgpu", "image", "advanced", "svg", "canvas", "hot", "debug", "lazy", "tokio"]
|
||||||
default-features = false
|
|
||||||
features = ["debug", "winit", "desktop", "winit_wgpu", "winit_tokio", "tokio", "wayland", "rfd", "dbus-config", "a11y", "wgpu", "multi-window", "process"]
|
|
||||||
|
|
||||||
[dependencies.iced_video_player]
|
[dependencies.iced_video_player]
|
||||||
git = "https://github.com/jackpot51/iced_video_player.git"
|
git = "https://git.tfcconnection.org/chris/iced_video_player"
|
||||||
branch = "cosmic"
|
branch = "master"
|
||||||
features = ["wgpu"]
|
# branch = "cosmic"
|
||||||
|
|
||||||
# [profile.dev]
|
# [profile.dev]
|
||||||
# opt-level = 3
|
# opt-level = 3
|
||||||
|
|
|
||||||
137
TODO.org
|
|
@ -1,137 +0,0 @@
|
||||||
#+TITLE: The Task list for Lumina
|
|
||||||
|
|
||||||
|
|
||||||
* 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 [#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.
|
|
||||||
|
|
||||||
Actually, what if we just made the svg at load/creation time and stored it in the file system for later, then load the entire songs svg's into memory during the presentation to speed things up? Would that be faster than creating them at on the fly? Is it the creation of them that is slow or the rendering?
|
|
||||||
|
|
||||||
** SVG performs badly
|
|
||||||
Since SVG's apparently run poorly in iced, instead I'll need to see about either creating a new text element, or teaching Iced to render strokes and shadows on text.
|
|
||||||
|
|
||||||
** Fork Cryoglyph
|
|
||||||
This fork will render text 3 times. Once for the text, once for the stroke, once for the shadow. This will only be used in the slides and therefore should not be much of a performance hit since we will only be render 3 copies of the given text. This should not be bad performance since it's not a large amount of text.
|
|
||||||
|
|
||||||
This also means in our custom widget with our custom fork, we can animate each individually perhaps.
|
|
||||||
|
|
||||||
** Actually.....
|
|
||||||
I tried out a way of generating the svg and rasterizing it ahead of time and then storing it in the file system to be cached. This works out very well. The text is one whole image for a slides text that gets layered on top of the background, but it works out well for now.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
* TODO [#C] Make the presenter more modular so things are easier to change. This is vague...
|
|
||||||
|
|
||||||
* TODO [#C] Figure out why the Video element seems to have problems when moving the mouse around
|
|
||||||
|
|
||||||
* DONE [#A] Create a view of all slides in a PDF presenation
|
|
||||||
|
|
||||||
* DONE [#A] Develop DnD for library items
|
|
||||||
This is limited by the fact that I need to develop this in cosmic. I am honestly thinking that I'll need to build my own drag and drop system or at least work with system76 to fix their dnd system on other systems.
|
|
||||||
|
|
||||||
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]
|
|
||||||
** DONE Library
|
|
||||||
** 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
|
|
||||||
|
|
||||||
* DONE Build library to see all available songs, images, videos, presentations, and slides
|
|
||||||
** DONE Develop ui for libraries
|
|
||||||
I've got the library basic layer done, I need to develop a way to open the libraries accordion button and then show the list of items in the library
|
|
||||||
** DONE Need to do search and creation systems yet
|
|
||||||
|
|
||||||
* DONE [#B] Build editors for each possible item
|
|
||||||
** DONE Develop ui for editors
|
|
||||||
|
|
||||||
* DONE [#B] Find a way to load and discover every font on the system for slide building
|
|
||||||
This may not be necessary since it is possible to create a font using =Box::leak()=.
|
|
||||||
#+begin_src rust
|
|
||||||
let font = self.current_slide.font().into_boxed_str();
|
|
||||||
let family = Family::Name(Box::leak(font));
|
|
||||||
let weight = Weight::Normal;
|
|
||||||
let stretch = Stretch::Normal;
|
|
||||||
let style = Style::Normal;
|
|
||||||
let font = Font {
|
|
||||||
family,
|
|
||||||
weight,
|
|
||||||
stretch,
|
|
||||||
style,
|
|
||||||
};
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
This code creates a font by leaking the Box to a ='static &str=. I just am not sure if the &str stays around in memory after the view function. If it does, then it's not on the stack anymore and should be fine, but if it isn't cleaned up then we will have a memory leak.
|
|
||||||
|
|
||||||
Krimzin on Discord told me that maybe the =update= method is a better place for this Box to be created or updated and then maybe I could generate the view from there.
|
|
||||||
|
|
||||||
* DONE Build an image editor
|
|
||||||
* DONE Use Rich Text instead of normal text for slides
|
|
||||||
This will make it so that we can add styling to the text like borders and backgrounds or highlights. Maybe in the future it'll add shadows too.
|
|
||||||
* DONE Build a video editor
|
|
||||||
* DONE Check into =mupdf-rs= for loading PDF's.
|
|
||||||
|
|
||||||
* DONE Build Menu
|
|
||||||
* DONE Find a way for text to pass through a service item to a slide i.e. content piece
|
|
||||||
This proved easier by just creating the =Slide= first and inserting it into the =ServiceItem=.
|
|
||||||
67
flake.lock
generated
|
|
@ -6,11 +6,11 @@
|
||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770794449,
|
"lastModified": 1755585599,
|
||||||
"narHash": "sha256-1nFkhcZx9+Sdw5OXwJqp5TxvGncqRqLeK781v0XV3WI=",
|
"narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "b19d93fdf9761e6101f8cb5765d638bacebd9a1b",
|
"rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -65,11 +65,11 @@
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769799857,
|
"lastModified": 1752689277,
|
||||||
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
|
"narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "naersk",
|
"repo": "naersk",
|
||||||
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
|
"rev": "0e72363d0938b0208d6c646d10649164c43f4d64",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -80,11 +80,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770562336,
|
"lastModified": 1755186698,
|
||||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
"narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
"rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -112,11 +112,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_3": {
|
"nixpkgs_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770562336,
|
"lastModified": 1755615617,
|
||||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -126,39 +126,22 @@
|
||||||
"type": "github"
|
"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": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"fenix": "fenix",
|
"fenix": "fenix",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"naersk": "naersk",
|
"naersk": "naersk",
|
||||||
"nixpkgs": "nixpkgs_3",
|
"nixpkgs": "nixpkgs_3"
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770702974,
|
"lastModified": 1755504847,
|
||||||
"narHash": "sha256-CbvWu72rpGHK5QynoXwuOnVzxX7njF2LYgk8wRSiAQ0=",
|
"narHash": "sha256-VX0B9hwhJypCGqncVVLC+SmeMVd/GAYbJZ0MiiUn2Pk=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "07a594815f7c1d6e7e39f21ddeeedb75b21795f4",
|
"rev": "a905e3b21b144d77e1b304e49f3264f6f8d4db75",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -185,24 +168,6 @@
|
||||||
"type": "github"
|
"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": {
|
"systems": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681028828,
|
"lastModified": 1681028828,
|
||||||
|
|
|
||||||
89
flake.nix
|
|
@ -6,43 +6,29 @@
|
||||||
naersk.url = "github:nix-community/naersk";
|
naersk.url = "github:nix-community/naersk";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
fenix.url = "github:nix-community/fenix";
|
fenix.url = "github:nix-community/fenix";
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs = inputs: with inputs;
|
||||||
inputs:
|
flake-utils.lib.eachDefaultSystem
|
||||||
with inputs;
|
(system:
|
||||||
flake-utils.lib.eachDefaultSystem (
|
|
||||||
system:
|
|
||||||
let
|
let
|
||||||
overlays = [ (import rust-overlay) ];
|
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system overlays;
|
inherit system;
|
||||||
# overlays = [ rust-overlay.overlays.default ];
|
overlays = [fenix.overlays.default];
|
||||||
# overlays = [cargo2nix.overlays.default];
|
# overlays = [cargo2nix.overlays.default];
|
||||||
};
|
};
|
||||||
naersk' = pkgs.callPackage naersk {};
|
naersk' = pkgs.callPackage naersk {};
|
||||||
|
nbi = with pkgs; [
|
||||||
# toolchain = (with pkgs.fenix.default; [cargo clippy rust-std rust-src rustc rustfmt rust-analyzer-nightly]);
|
|
||||||
|
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
|
||||||
# Rust tools
|
# Rust tools
|
||||||
# toolchain
|
alejandra
|
||||||
# (pkgs.fenix.default.withComponents [
|
(pkgs.fenix.stable.withComponents [
|
||||||
# "cargo"
|
"cargo"
|
||||||
# "clippy"
|
"clippy"
|
||||||
# "rust-std"
|
"rust-src"
|
||||||
# # "rust-src"
|
"rustc"
|
||||||
# "rustc"
|
"rustfmt"
|
||||||
# "rustfmt"
|
])
|
||||||
# ])
|
rust-analyzer
|
||||||
(rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
|
|
||||||
extensions = [ "rust-src" "rust-analyzer" "clippy" ];
|
|
||||||
}))
|
|
||||||
cargo-nextest
|
|
||||||
cargo-criterion
|
|
||||||
# rust-analyzer-nightly
|
|
||||||
vulkan-loader
|
vulkan-loader
|
||||||
wayland
|
wayland
|
||||||
wayland-protocols
|
wayland-protocols
|
||||||
|
|
@ -51,22 +37,19 @@
|
||||||
sccache
|
sccache
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
bi = with pkgs; [
|
||||||
gcc
|
gcc
|
||||||
stdenv
|
stdenv
|
||||||
gnumake
|
gnumake
|
||||||
gdb
|
gdb
|
||||||
lldb
|
lldb
|
||||||
cmake
|
cmake
|
||||||
clang
|
|
||||||
libclang
|
|
||||||
makeWrapper
|
makeWrapper
|
||||||
vulkan-headers
|
vulkan-headers
|
||||||
vulkan-loader
|
vulkan-loader
|
||||||
vulkan-tools
|
vulkan-tools
|
||||||
libGL
|
libGL
|
||||||
cargo-flamegraph
|
cargo-flamegraph
|
||||||
bacon
|
|
||||||
|
|
||||||
fontconfig
|
fontconfig
|
||||||
glib
|
glib
|
||||||
|
|
@ -79,58 +62,38 @@
|
||||||
gst_all_1.gst-plugins-rs
|
gst_all_1.gst-plugins-rs
|
||||||
gst_all_1.gst-vaapi
|
gst_all_1.gst-vaapi
|
||||||
gst_all_1.gstreamer
|
gst_all_1.gstreamer
|
||||||
|
# podofo
|
||||||
|
# mpv
|
||||||
ffmpeg-full
|
ffmpeg-full
|
||||||
mupdf
|
|
||||||
# yt-dlp
|
# yt-dlp
|
||||||
|
|
||||||
just
|
just
|
||||||
sqlx-cli
|
sqlx-cli
|
||||||
cargo-watch
|
cargo-watch
|
||||||
samply
|
|
||||||
];
|
];
|
||||||
|
in rec
|
||||||
|
{
|
||||||
|
devShell = pkgs.mkShell.override {
|
||||||
|
# stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv;
|
||||||
|
} {
|
||||||
|
nativeBuildInputs = nbi;
|
||||||
|
buildInputs = bi;
|
||||||
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${
|
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${
|
||||||
with pkgs;
|
with pkgs;
|
||||||
pkgs.lib.makeLibraryPath [
|
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.vulkan-loader
|
||||||
pkgs.wayland
|
pkgs.wayland
|
||||||
pkgs.wayland-protocols
|
pkgs.wayland-protocols
|
||||||
pkgs.libxkbcommon
|
pkgs.libxkbcommon
|
||||||
pkgs.mupdf
|
|
||||||
pkgs.libclang
|
|
||||||
]
|
]
|
||||||
}";
|
}";
|
||||||
in
|
DATABASE_URL = "sqlite:///home/chris/.local/share/lumina/library-db.sqlite3";
|
||||||
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 {
|
defaultPackage = naersk'.buildPackage {
|
||||||
inherit nativeBuildInputs buildInputs LD_LIBRARY_PATH;
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
};
|
};
|
||||||
packages = {
|
packages = {
|
||||||
default = naersk'.buildPackage {
|
default = naersk'.buildPackage {
|
||||||
inherit nativeBuildInputs buildInputs LD_LIBRARY_PATH;
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
33
justfile
|
|
@ -1,37 +1,24 @@
|
||||||
ui := "-i"
|
ui := "-i"
|
||||||
verbose := "-v"
|
|
||||||
file := "~/dev/lumina-iced/test_presentation.lisp"
|
file := "~/dev/lumina-iced/test_presentation.lisp"
|
||||||
|
|
||||||
export RUSTC_WRAPPER := "sccache"
|
|
||||||
# export RUST_LOG := "debug"
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
just --list
|
just --list
|
||||||
build:
|
build:
|
||||||
cargo build
|
RUST_LOG=debug cargo build
|
||||||
build-release:
|
sbuild:
|
||||||
cargo build --release
|
RUST_LOG=debug sccache cargo build
|
||||||
run:
|
run:
|
||||||
cargo run -- {{verbose}} {{ui}}
|
RUST_LOG=debug cargo run -- {{ui}} {{file}}
|
||||||
run-release:
|
srun:
|
||||||
cargo run --release -- {{verbose}} {{ui}}
|
RUST_LOG=debug sccache cargo run -- {{ui}} {{file}}
|
||||||
run-file:
|
|
||||||
cargo run -- {{verbose}} {{ui}} {{file}}
|
|
||||||
clean:
|
clean:
|
||||||
cargo clean
|
RUST_LOG=debug cargo clean
|
||||||
test:
|
test:
|
||||||
cargo nextest run
|
RUST_LOG=debug cargo test --benches --tests --all-features -- --nocapture
|
||||||
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
|
|
||||||
profile:
|
profile:
|
||||||
samply record cargo run --release -- {{verbose}} {{ui}}
|
cargo flamegraph --image-width 8000 -- {{ui}} {{file}}
|
||||||
|
|
||||||
alias b := build
|
alias b := build
|
||||||
alias r := run
|
alias r := run
|
||||||
alias br := build-release
|
alias sr := srun
|
||||||
alias rr := run-release
|
|
||||||
alias rf := run-file
|
|
||||||
alias c := clean
|
alias c := clean
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
-- Add migration script here
|
|
||||||
ALTER TABLE presentations
|
|
||||||
DROP COLUMN pageCount;
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
-- Add migration script here
|
|
||||||
ALTER TABLE presentations
|
|
||||||
ADD COLUMN starting_index INTEGER;
|
|
||||||
|
|
||||||
ALTER TABLE presentations
|
|
||||||
ADD COLUMN ending_index INTEGER;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -4,12 +4,14 @@
|
||||||
Lumina is a presentation app that works from a cli or a UI. The goal is that through a simple text file, you can describe an entire presentation and then load and control it either from the command line, or a UI. The UI also provides user friendly ways of creating the presentation to allow for flexibility for users to make something that works for regular folk as well as developers and nerds.
|
Lumina is a presentation app that works from a cli or a UI. The goal is that through a simple text file, you can describe an entire presentation and then load and control it either from the command line, or a UI. The UI also provides user friendly ways of creating the presentation to allow for flexibility for users to make something that works for regular folk as well as developers and nerds.
|
||||||
|
|
||||||
* Why build this?
|
* Why build this?
|
||||||
|
Well for one, I want more experience developing things and I don't have a good tool for this kind of thing on Linux.
|
||||||
|
|
||||||
Primarily, I don't think there is a good tool for this kind of thing on Linux. On Windows and Mac there is ProPresenter or Proclaim. Both amazing presentation software built for churches or worship centers and can be used by others for other things too, but incredible tools. I want to have a similar tool on Linux. The available tools out there now are often old, broken, or very difficult to use. I want something incredibly easy, with very sane or at least very customizable keyboard controls that allow me to quickly build a presentation and make it VERY easy to run it too.
|
Primarily, I don't think there is a good tool for this kind of thing on Linux. On Windows and Mac there is ProPresenter or Proclaim. Both amazing presentation software built for churches or worship centers and can be used by others for other things too, but incredible tools. I want to have a similar tool on Linux. The available tools out there now are often old, broken, or very difficult to use. I want something incredibly easy, with very sane or at least very customizable keyboard controls that allow me to quickly build a presentation and make it VERY easy to run it too.
|
||||||
|
|
||||||
** Features (planned are in parentheses)
|
** Features (planned are in parentheses)
|
||||||
- Presents songs lyrics with image and video backgrounds
|
- Presents songs lyrics with image and video backgrounds
|
||||||
- Simple song creation with a powerful text parser
|
- Simple song creation with a powerful text parser
|
||||||
- Present Slides. PDF works. (PowerPoint, and Impress are in not implemented yet)
|
- Present Slides (PDF, PowerPoint, and Impress are in not implemented yet)
|
||||||
- (Present Reveal.js slides)
|
- (Present Reveal.js slides)
|
||||||
- (Custom slide builder)
|
- (Custom slide builder)
|
||||||
- (an intuitive UI) - Still needs A LOT of polish
|
- (an intuitive UI) - Still needs A LOT of polish
|
||||||
|
|
|
||||||
BIN
res/chad.png
|
Before Width: | Height: | Size: 673 KiB |
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 76 76" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" enable-background="new 0 0 76.00 76.00" xml:space="preserve">
|
|
||||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 19,35L 19,43L 27,43L 27,35L 19,35 Z M 30.9999,35L 30.9999,43L 56.9998,43L 56.9999,35L 30.9999,35 Z M 31,32L 57,32L 57,24L 31,24L 31,32 Z M 32,31L 32,25L 56,25L 56,31L 32,31 Z M 21,33L 25,33L 25,30L 28,30L 28,26L 25,26L 25,23L 21,23L 21,26L 18,26L 18,30L 21,30L 21,33 Z M 22,32L 22,29L 19,29L 19,27L 22,27L 22,24L 24,24L 24,27L 27,27L 27,29L 24,29L 24,32L 22,32 Z M 19.0001,46L 19.0001,54L 27.0001,54L 27.0001,46L 19.0001,46 Z M 31.0001,46L 31.0001,54L 57,54L 57,46L 31.0001,46 Z "/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1,022 B |
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 76 76" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" enable-background="new 0 0 76.00 76.00" xml:space="preserve">
|
|
||||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 19,42L 19,34L 27,34L 27,42L 19,42 Z M 30.9999,42L 30.9999,34L 56.9999,34L 56.9999,42L 30.9999,42 Z M 31,45L 57,45L 57,53L 31,53L 31,45 Z M 32,46L 32,52L 56,52L 56,46L 32,46 Z M 21,44L 25,44L 25,47L 28,47L 28,51L 25,51L 25,54L 21,54L 21,51L 18,51L 18,47L 21,47L 21,44 Z M 22,45L 22,48L 19,48L 19,50L 22,50L 22,53L 24,53L 24,50L 27,50L 27,48L 24,48L 24,45L 22,45 Z M 19.0001,31L 19.0001,23L 27.0001,23L 27.0001,31L 19.0001,31 Z M 31.0001,31L 31.0001,23L 57,23L 57,31L 31.0001,31 Z "/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1,022 B |
BIN
res/nerdfont.ttf
|
|
@ -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,54 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="800px"
|
|
||||||
height="800px"
|
|
||||||
viewBox="0 0 76 76"
|
|
||||||
version="1.1"
|
|
||||||
enable-background="new 0 0 76.00 76.00"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="split-above.svg"
|
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs1" /><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.74629807"
|
|
||||||
inkscape:cx="252.58004"
|
|
||||||
inkscape:cy="447.54236"
|
|
||||||
inkscape:window-width="1463"
|
|
||||||
inkscape:window-height="909"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg1" />
|
|
||||||
<path
|
|
||||||
fill="#000000"
|
|
||||||
fill-opacity="1"
|
|
||||||
stroke-width="0.404256"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M 3.8256403,28.314338 V 46.261937 H 18.394593 V 28.314338 Z m 21.8532467,0 v 17.947599 h 47.348916 l 1.82e-4,-17.947599 z m 1.83e-4,-6.730349 H 73.028167 V 3.6363892 H 25.67907 Z m 1.821118,-2.243451 V 5.8798398 h 43.70686 V 19.340538 Z M 3.8258225,52.992286 V 70.939887 H 18.394774 V 52.992286 Z m 21.8534285,0 V 70.939887 H 73.028167 V 52.992286 Z"
|
|
||||||
id="path1"
|
|
||||||
sodipodi:nodetypes="cccccccccccccccccccccccccccccc" />
|
|
||||||
<path
|
|
||||||
d="M 3.5834285,21.523408 H 18.345395 V 3.5758086 H 3.5834285 Z M 5.3255032,19.102699 V 5.7129051 L 16.732439,5.5582305 16.783963,19.315409 Z"
|
|
||||||
style="fill:#000000;fill-opacity:1;stroke-width:0.225723;stroke-linejoin:round"
|
|
||||||
id="path1-2"
|
|
||||||
sodipodi:nodetypes="cccccccccc" /><rect
|
|
||||||
style="fill:#000000;stroke-width:0.11759"
|
|
||||||
id="rect1"
|
|
||||||
width="72.699356"
|
|
||||||
height="1.642724"
|
|
||||||
x="1.9440452"
|
|
||||||
y="24.184471" /></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB |
|
|
@ -1,60 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="800px"
|
|
||||||
height="800px"
|
|
||||||
viewBox="0 0 76 76"
|
|
||||||
version="1.1"
|
|
||||||
enable-background="new 0 0 76.00 76.00"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="split-below.svg"
|
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs1" /><sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.76425252"
|
|
||||||
inkscape:cx="252.53433"
|
|
||||||
inkscape:cy="367.67952"
|
|
||||||
inkscape:window-width="1463"
|
|
||||||
inkscape:window-height="909"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg1" />
|
|
||||||
<path
|
|
||||||
fill="#000000"
|
|
||||||
fill-opacity="1"
|
|
||||||
stroke-width="0.404256"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M 3.8256403,28.314338 V 46.261937 H 18.394593 V 28.314338 Z m 21.8532467,0 v 17.947599 h 47.348916 l 1.82e-4,-17.947599 z"
|
|
||||||
id="path1"
|
|
||||||
sodipodi:nodetypes="cccccccccc" />
|
|
||||||
<path
|
|
||||||
d="M 3.8,71.717599 H 18.561966 V 53.77 H 3.8 Z M 5.5420747,69.29689 V 55.907096 L 16.94901,55.752421 17.00053,69.5096 Z"
|
|
||||||
style="fill:#000000;fill-opacity:1;stroke-width:0.225723;stroke-linejoin:round"
|
|
||||||
id="path1-2"
|
|
||||||
sodipodi:nodetypes="cccccccccc" /><rect
|
|
||||||
style="fill:#000000;stroke-width:0.11759"
|
|
||||||
id="rect1"
|
|
||||||
width="72.699356"
|
|
||||||
height="1.642724"
|
|
||||||
x="1.9440452"
|
|
||||||
y="49.006992" /><path
|
|
||||||
d="M 3.8,3.924661 V 21.872262 H 18.368951 V 3.924661 Z m 21.853428,0 V 21.872262 H 73.002344 V 3.924661 Z"
|
|
||||||
style="fill:#000000;fill-opacity:1;stroke-width:0.404256;stroke-linejoin:round"
|
|
||||||
id="path1-7" /><path
|
|
||||||
d="M 25.65,71.7176 H 72.999097 V 53.77 H 25.65 Z m 1.821118,-2.243451 V 56.013451 h 43.70686 v 13.460698 z"
|
|
||||||
style="fill:#000000;fill-opacity:1;stroke-width:0.404256;stroke-linejoin:round"
|
|
||||||
id="path1-6" /></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>Text-effects-24-regular SVG Icon</title><path fill="currentColor" d="M14.298 4.015a2.5 2.5 0 0 0-4.595 0l-4.264 9.95q-.023.05-.045.103l-1.691 3.947a2.5 2.5 0 1 0 4.596 1.97L9.363 17.5h5.275l1.065 2.485a2.5 2.5 0 0 0 4.596-1.97l-1.692-3.947l-.045-.104zM12.495 12.5l-.494-1.153l-.495 1.153zm.425-7.894l4.277 9.98l.018.041l1.705 3.98a1 1 0 0 1-1.839.787L15.627 16H8.374L6.92 19.394a1 1 0 1 1-1.839-.788l1.706-3.979l.017-.041l4.277-9.98a1 1 0 0 1 1.839 0M14.77 14H9.23L12 7.539z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 573 B |
|
|
@ -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 |
|
|
@ -1,3 +1,3 @@
|
||||||
max_width = 70
|
max_width = 70
|
||||||
style_edition = "2024"
|
# style_edition = "2018"
|
||||||
# version = "Two"
|
# version = "Two"
|
||||||
764
src/core/file.rs
|
|
@ -1,534 +1,356 @@
|
||||||
use crate::core::{
|
|
||||||
kinds::ServiceItemKind, service_items::ServiceItem,
|
|
||||||
slide::Background,
|
|
||||||
};
|
|
||||||
use cosmic::widget::image::Handle;
|
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
|
||||||
use std::{
|
|
||||||
fs::{self, File},
|
|
||||||
io::Write,
|
|
||||||
iter,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
use tar::{Archive, Builder};
|
use tar::{Archive, Builder};
|
||||||
use tracing::{debug, error};
|
use tracing::error;
|
||||||
use zstd::{Decoder, Encoder};
|
use zstd::Encoder;
|
||||||
|
use std::{fs::{self, File}, iter, path::{Path, PathBuf}};
|
||||||
|
use color_eyre::eyre::{eyre, Context, Result};
|
||||||
|
use serde_json::Value;
|
||||||
|
use sqlx::{query, query_as, FromRow, SqliteConnection};
|
||||||
|
use crate::{images::{get_image_from_db, Image}, kinds::ServiceItemKind, model::get_db, presentations::{get_presentation_from_db, PresKind, Presentation}, service_items::ServiceItem, slides::Background, songs::{get_song_from_db, Song}, videos::{get_video_from_db, Video}};
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
pub async fn save(list: Vec<ServiceItem>, path: impl AsRef<Path>) -> Result<()> {
|
||||||
pub fn save(
|
|
||||||
list: Vec<ServiceItem>,
|
|
||||||
path: impl AsRef<Path>,
|
|
||||||
overwrite: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
if overwrite && path.exists() {
|
let save_file = File::create(path)?;
|
||||||
fs::remove_file(path).into_diagnostic()?;
|
let mut db = get_db().await;
|
||||||
|
let json = process_service_items(&list, &mut db).await?;
|
||||||
|
let archive = store_service_items(&list, &mut db, &save_file, &json).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
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 encoder = Encoder::new(save_file, 3)
|
async fn store_service_items(items: &Vec<ServiceItem>, db: &mut SqliteConnection, save_file: &File, json: &Value) -> Result<()> {
|
||||||
.expect("file encoder shouldn't fail")
|
let encoder = Encoder::new(save_file, 3).unwrap();
|
||||||
.auto_finish();
|
|
||||||
let mut tar = Builder::new(encoder);
|
let mut tar = Builder::new(encoder);
|
||||||
let mut temp_dir = dirs::data_dir().expect(
|
let mut temp_dir = dirs::data_dir().unwrap();
|
||||||
"there should be a data directory, ~/.local/share/ for linux, but couldn't find it",
|
|
||||||
);
|
|
||||||
temp_dir.push("lumina");
|
temp_dir.push("lumina");
|
||||||
let mut s: String =
|
let mut s: String =
|
||||||
iter::repeat_with(fastrand::alphanumeric).take(5).collect();
|
iter::repeat_with(fastrand::alphanumeric)
|
||||||
|
.take(5)
|
||||||
|
.collect();
|
||||||
s.insert_str(0, "temp_");
|
s.insert_str(0, "temp_");
|
||||||
temp_dir.push(s);
|
temp_dir.push(s);
|
||||||
fs::create_dir_all(&temp_dir).into_diagnostic()?;
|
fs::create_dir_all(&temp_dir)?;
|
||||||
let service_file = temp_dir.join("serviceitems.ron");
|
let service_file = temp_dir.join("serviceitems.json");
|
||||||
debug!(?service_file);
|
fs::File::create(&service_file)?;
|
||||||
fs::File::create(&service_file).into_diagnostic()?;
|
match fs::File::options().read(true).write(true).open(service_file) {
|
||||||
match fs::File::options()
|
Ok(f) => {
|
||||||
.read(true)
|
serde_json::to_writer_pretty(f, json)?;
|
||||||
.write(true)
|
},
|
||||||
.open(service_file)
|
Err(e) => error!("There were problems making a file i guess: {e}"),
|
||||||
{
|
|
||||||
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}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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(())
|
|
||||||
};
|
};
|
||||||
|
for item in items {
|
||||||
for item in list {
|
|
||||||
let background;
|
let background;
|
||||||
let audio: Option<PathBuf>;
|
let audio: Option<PathBuf>;
|
||||||
match &item.kind {
|
match item.kind {
|
||||||
ServiceItemKind::Song(song) => {
|
ServiceItemKind::Song => {
|
||||||
background = song.background.clone();
|
let song = get_song_from_db(item.database_id, db).await?;
|
||||||
audio = song.audio.clone();
|
background = song.background;
|
||||||
}
|
audio = song.audio;
|
||||||
ServiceItemKind::Image(image) => {
|
},
|
||||||
background = Some(
|
ServiceItemKind::Image => {
|
||||||
Background::try_from(image.path.clone())
|
let image = get_image_from_db(item.database_id, db).await?;
|
||||||
.into_diagnostic()?,
|
background = Some(Background::try_from(image.path)?);
|
||||||
);
|
|
||||||
audio = None;
|
audio = None;
|
||||||
}
|
},
|
||||||
ServiceItemKind::Video(video) => {
|
ServiceItemKind::Video => {
|
||||||
background = Some(
|
let video = get_video_from_db(item.database_id, db).await?;
|
||||||
Background::try_from(video.path.clone())
|
background = Some(Background::try_from(video.path)?);
|
||||||
.into_diagnostic()?,
|
|
||||||
);
|
|
||||||
audio = None;
|
audio = None;
|
||||||
}
|
},
|
||||||
ServiceItemKind::Presentation(presentation) => {
|
ServiceItemKind::Presentation(_) => {
|
||||||
background = Some(
|
let presentation = get_presentation_from_db(item.database_id, db).await?;
|
||||||
Background::try_from(presentation.path.clone())
|
background = Some(Background::try_from(presentation.path)?);
|
||||||
.into_diagnostic()?,
|
|
||||||
);
|
|
||||||
audio = None;
|
audio = None;
|
||||||
|
},
|
||||||
|
ServiceItemKind::Content => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
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).wrap_err("Couldn't create audio file")?;
|
||||||
|
fs::copy(file, audio_file).wrap_err("Audio file could not be copied, the source file doesn't exist not be found");
|
||||||
|
} else {
|
||||||
|
fs::File::create(&audio_file).wrap_err("Couldn't create audio file")?;
|
||||||
|
fs::copy(file, audio_file).wrap_err("Audio file could not be copied, the source file doesn't exist not be found");
|
||||||
}
|
}
|
||||||
ServiceItemKind::Content(_slide) => {
|
};
|
||||||
|
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).wrap_err("Couldn't create background file")?;
|
||||||
|
fs::copy(file, background_file).wrap_err("Background file could not be copied, the source file doesn't exist not be found");
|
||||||
|
} else {
|
||||||
|
fs::File::create(&background_file).wrap_err("Couldn't create background file")?;
|
||||||
|
fs::copy(file.path, background_file).wrap_err("Background file could not be copied, the source file doesn't exist not be found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_temp_dir(temp_dir: &Path) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some(path) = audio
|
async fn process_service_items(items: &Vec<ServiceItem>, db: &mut SqliteConnection) -> Result<Value> {
|
||||||
&& path.exists()
|
let mut values: Vec<Value> = vec![];
|
||||||
{
|
for item in items {
|
||||||
debug!(?path);
|
match item.kind {
|
||||||
append_file(path)?;
|
ServiceItemKind::Song => {
|
||||||
}
|
let value = process_song(item.database_id, db).await?;
|
||||||
if let Some(background) = background
|
values.push(value);
|
||||||
&& let path = background.path
|
},
|
||||||
&& path.exists()
|
ServiceItemKind::Image => {
|
||||||
{
|
let value = process_image(item.database_id, db).await?;
|
||||||
debug!(?path);
|
values.push(value);
|
||||||
append_file(path)?;
|
},
|
||||||
}
|
ServiceItemKind::Video => {
|
||||||
for slide in item.slides {
|
let value = process_video(item.database_id, db).await?;
|
||||||
if let Some(svg) = slide.text_svg
|
values.push(value);
|
||||||
&& let Some(path) = svg.path
|
},
|
||||||
{
|
ServiceItemKind::Presentation(_) => {
|
||||||
append_file(path)?;
|
let value = process_presentation(item.database_id, db).await?;
|
||||||
|
values.push(value);
|
||||||
|
},
|
||||||
|
ServiceItemKind::Content => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let json = Value::from(values);
|
||||||
|
Ok(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
match tar.finish() {
|
async fn process_song(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
|
||||||
Ok(()) => (),
|
let song = get_song_from_db(database_id, db).await?;
|
||||||
Err(e) => {
|
let song_json = serde_json::to_value(&song)?;
|
||||||
error!(?e);
|
let kind_json = serde_json::to_value(ServiceItemKind::Song)?;
|
||||||
return Err(miette!("tar error: {e}"));
|
let json = serde_json::json!({"item": song_json, "kind": kind_json});
|
||||||
}
|
Ok(json)
|
||||||
}
|
|
||||||
fs::remove_dir_all(temp_dir).into_diagnostic()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
async fn process_image(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
|
||||||
pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
let image = get_image_from_db(database_id, db).await?;
|
||||||
let decoder =
|
let image_json = serde_json::to_value(&image)?;
|
||||||
Decoder::new(fs::File::open(&path).into_diagnostic()?)
|
let kind_json = serde_json::to_value(ServiceItemKind::Image)?;
|
||||||
.into_diagnostic()?;
|
let json = serde_json::json!({"item": image_json, "kind": kind_json});
|
||||||
let mut tar = Archive::new(decoder);
|
Ok(json)
|
||||||
|
|
||||||
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()?;
|
async fn process_video(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
|
||||||
let ron_file = dir
|
let video = get_video_from_db(database_id, db).await?;
|
||||||
.find_map(|file| {
|
let video_json = serde_json::to_value(&video)?;
|
||||||
if file.as_ref().ok()?.path().extension()?.to_str()?
|
let kind_json = serde_json::to_value(ServiceItemKind::Video)?;
|
||||||
== "ron"
|
let json = serde_json::json!({"item": video_json, "kind": kind_json});
|
||||||
{
|
Ok(json)
|
||||||
Some(file.ok()?.path())
|
|
||||||
} 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 {
|
async fn process_presentation(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
|
||||||
ServiceItemKind::Song(song) => {
|
let presentation = get_presentation_from_db(database_id, db).await?;
|
||||||
if let Ok(file) = file.as_ref() {
|
let presentation_json = serde_json::to_value(&presentation)?;
|
||||||
let file_name = file.file_name();
|
let kind_json = match presentation.kind {
|
||||||
let audio_path =
|
PresKind::Html => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Html))?,
|
||||||
song.audio.clone().unwrap_or_default();
|
PresKind::Pdf => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Pdf))?,
|
||||||
if Some(file_name.as_os_str())
|
PresKind::Generic => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Generic))?,
|
||||||
== song
|
};
|
||||||
.background
|
let json = serde_json::json!({"item": presentation_json, "kind": kind_json});
|
||||||
.clone()
|
Ok(json)
|
||||||
.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!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(items)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
use std::path::PathBuf;
|
||||||
use resvg::usvg::fontdb;
|
|
||||||
|
|
||||||
|
use fs::canonicalize;
|
||||||
|
use sqlx::Connection;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use tracing::debug;
|
||||||
use super::*;
|
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 {
|
async fn get_db() -> SqliteConnection {
|
||||||
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 mut data = dirs::data_local_dir().unwrap();
|
||||||
let verse_map: Option<HashMap<VerseName, String>> =
|
data.push("lumina");
|
||||||
ron::from_str(&lyrics).unwrap();
|
data.push("library-db.sqlite3");
|
||||||
Song {
|
let mut db_url = String::from("sqlite://");
|
||||||
id: 7,
|
db_url.push_str(data.to_str().unwrap());
|
||||||
title: "Death Was Arrested".to_string(),
|
SqliteConnection::connect(&db_url)
|
||||||
lyrics: Some(lyrics),
|
.await
|
||||||
author: Some(
|
.expect("problems")
|
||||||
"North Point Worship".to_string(),
|
}
|
||||||
),
|
|
||||||
ccli: None,
|
#[tokio::test(flavor = "current_thread")]
|
||||||
audio: Some("/home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3".into()),
|
async fn test_process_song() {
|
||||||
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()]),
|
let mut db = get_db().await;
|
||||||
background: Some(Background::try_from("/home/chris/nc/tfc/openlp/Flood/motions/Ocean_Floor_HD.mp4").unwrap()),
|
let result = process_song(7, &mut db).await;
|
||||||
text_alignment: Some(TextAlignment::MiddleCenter),
|
let json_song_file = PathBuf::from("./test/test_song.json");
|
||||||
font: None,
|
if let Ok(path) = canonicalize(json_song_file) {
|
||||||
font_size: Some(120),
|
debug!(file = ?&path);
|
||||||
font_style: None,
|
if let Ok(s) = fs::read_to_string(path) {
|
||||||
font_weight: None,
|
debug!(s);
|
||||||
text_color: None,
|
match result {
|
||||||
stroke_size: None,
|
Ok(json) => assert_eq!(json.to_string(), s),
|
||||||
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 }
|
Err(e) => panic!("There was an error in processing the song: {e}"),
|
||||||
]),
|
}
|
||||||
verse_map,
|
} else {
|
||||||
..Default::default()
|
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> {
|
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![
|
let items = vec![
|
||||||
ServiceItem {
|
ServiceItem {
|
||||||
database_id: 7,
|
database_id: 7,
|
||||||
kind: ServiceItemKind::Song(song.clone()),
|
kind: ServiceItemKind::Song,
|
||||||
id: 0,
|
id: 0,
|
||||||
title: "Death was Arrested".into(),
|
|
||||||
slides: slides.clone(),
|
|
||||||
},
|
},
|
||||||
ServiceItem {
|
ServiceItem {
|
||||||
database_id: 7,
|
database_id: 54,
|
||||||
kind: ServiceItemKind::Song(song),
|
kind: ServiceItemKind::Presentation(PresKind::Html),
|
||||||
id: 1,
|
id: 0,
|
||||||
title: "Death was Arrested".into(),
|
},
|
||||||
slides: slides,
|
ServiceItem {
|
||||||
|
database_id: 73,
|
||||||
|
kind: ServiceItemKind::Video,
|
||||||
|
id: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn test_load() -> Result<(), String> {
|
async fn test_service_items() {
|
||||||
test_save();
|
let mut db = get_db().await;
|
||||||
let path = PathBuf::from("./test.pres");
|
let items = get_items();
|
||||||
let result = load(&path);
|
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 {
|
match result {
|
||||||
Ok(items) => {
|
Ok(strings) => assert_eq!(strings.to_string(), s),
|
||||||
assert!(items.len() > 0);
|
Err(e) => panic!("There was an error: {e}"),
|
||||||
// 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> {
|
// #[tokio::test]
|
||||||
let cache_dir = cache_dir();
|
// async fn test_save() {
|
||||||
items.iter().try_for_each(|item| {
|
// let path = PathBuf::from("~/dev/lumina/src/rust/core/test.pres");
|
||||||
if let ServiceItemKind::Song(..) = item.kind {
|
// let list = get_items();
|
||||||
item.slides.iter().try_for_each(|slide| {
|
// match save(list, path).await {
|
||||||
slide.text_svg.as_ref().map_or(Err(String::from("There is no TextSvg for this song")), |text_svg| {
|
// Ok(_) => assert!(true),
|
||||||
|
// Err(e) => panic!("There was an error: {e}"),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if text_svg.handle.is_none() {
|
#[tokio::test]
|
||||||
return Err(String::from("There is no handle in this song's TextSvg"));
|
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;
|
||||||
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"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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();
|
let list = get_items();
|
||||||
match save(list, &path, true) {
|
if let Ok(json) = process_service_items(&list, &mut db).await {
|
||||||
Ok(_) => {
|
println!("{:?}", json);
|
||||||
assert!(path.is_file());
|
match store_service_items(&list, &mut db, &save_file, &json).await {
|
||||||
let Ok(file) = fs::File::open(path) else {
|
Ok(_) => assert!(true),
|
||||||
return assert!(false, "couldn't open file");
|
Err(e) => panic!("There was an error: {e}"),
|
||||||
};
|
|
||||||
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}"),
|
} 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");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
|
pool::PoolConnection, query, query_as, Sqlite, SqliteConnection,
|
||||||
query, query_as,
|
SqlitePool,
|
||||||
};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use tracing::{debug, error};
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
||||||
)]
|
)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
|
@ -25,30 +25,8 @@ pub struct Image {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PathBuf> for Image {
|
|
||||||
fn from(value: PathBuf) -> Self {
|
|
||||||
let title = value
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_str()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string();
|
|
||||||
Self {
|
|
||||||
id: 0,
|
|
||||||
title,
|
|
||||||
path: value.canonicalize().unwrap_or(value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Path> for Image {
|
|
||||||
fn from(value: &Path) -> Self {
|
|
||||||
Self::from(value.to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Image> for Value {
|
impl From<&Image> for Value {
|
||||||
fn from(_value: &Image) -> Self {
|
fn from(value: &Image) -> Self {
|
||||||
Self::List(vec![Self::Symbol(Symbol("image".into()))])
|
Self::List(vec![Self::Symbol(Symbol("image".into()))])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,10 +50,10 @@ impl Content for Image {
|
||||||
|
|
||||||
fn subtext(&self) -> String {
|
fn subtext(&self) -> String {
|
||||||
if self.path.exists() {
|
if self.path.exists() {
|
||||||
self.path.file_name().map_or_else(
|
self.path
|
||||||
|| "Missing image".into(),
|
.file_name()
|
||||||
|f| f.to_string_lossy().to_string(),
|
.map(|f| f.to_string_lossy().to_string())
|
||||||
)
|
.unwrap_or("Missing image".into())
|
||||||
} else {
|
} else {
|
||||||
"Missing image".into()
|
"Missing image".into()
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +66,6 @@ impl From<Value> for Image {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::option_if_let_else)]
|
|
||||||
impl From<&Value> for Image {
|
impl From<&Value> for Image {
|
||||||
fn from(value: &Value) -> Self {
|
fn from(value: &Value) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
@ -108,7 +85,7 @@ impl From<&Value> for Image {
|
||||||
let path =
|
let path =
|
||||||
p.to_str().unwrap_or_default().to_string();
|
p.to_str().unwrap_or_default().to_string();
|
||||||
let title =
|
let title =
|
||||||
path.rsplit_once('/').unwrap_or_default().1;
|
path.rsplit_once("/").unwrap_or_default().1;
|
||||||
title.to_string()
|
title.to_string()
|
||||||
});
|
});
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -177,50 +154,16 @@ impl Model<Image> {
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
for image in v {
|
for image in v.into_iter() {
|
||||||
let _ = self.add_item(image);
|
let _ = self.add_item(image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!("There was an error in converting images: {e}")
|
||||||
"There was an error in converting images: {e}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove_from_db(
|
|
||||||
db: PoolConnection<Sqlite>,
|
|
||||||
id: i32,
|
|
||||||
) -> Result<()> {
|
|
||||||
query!("DELETE FROM images WHERE id = $1", id)
|
|
||||||
.execute(&mut db.detach())
|
|
||||||
.await
|
|
||||||
.into_diagnostic()
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_image_to_db(
|
|
||||||
image: Image,
|
|
||||||
db: PoolConnection<Sqlite>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let path = image
|
|
||||||
.path
|
|
||||||
.to_str()
|
|
||||||
.map(std::string::ToString::to_string)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let mut db = db.detach();
|
|
||||||
query!(
|
|
||||||
r#"INSERT INTO images (title, file_path) VALUES ($1, $2)"#,
|
|
||||||
image.title,
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
.execute(&mut db)
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_image_in_db(
|
pub async fn update_image_in_db(
|
||||||
image: Image,
|
image: Image,
|
||||||
|
|
@ -229,30 +172,20 @@ pub async fn update_image_in_db(
|
||||||
let path = image
|
let path = image
|
||||||
.path
|
.path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(std::string::ToString::to_string)
|
.map(|s| s.to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mut db = db.detach();
|
query!(
|
||||||
debug!(?image, "should be been updated");
|
|
||||||
let result = query!(
|
|
||||||
r#"UPDATE images SET title = $2, file_path = $3 WHERE id = $1"#,
|
r#"UPDATE images SET title = $2, file_path = $3 WHERE id = $1"#,
|
||||||
image.id,
|
image.id,
|
||||||
image.title,
|
image.title,
|
||||||
path,
|
path,
|
||||||
)
|
)
|
||||||
.execute(&mut db)
|
.execute(&mut db.detach())
|
||||||
.await.into_diagnostic();
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => {
|
|
||||||
debug!("should have been updated");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
error! {?e};
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_image_from_db(
|
pub async fn get_image_from_db(
|
||||||
database_id: i32,
|
database_id: i32,
|
||||||
|
|
@ -269,9 +202,7 @@ mod test {
|
||||||
fn test_image(title: String) -> Image {
|
fn test_image(title: String) -> Image {
|
||||||
Image {
|
Image {
|
||||||
title,
|
title,
|
||||||
path: PathBuf::from(
|
path: PathBuf::from("~/pics/camprules2024.mp4"),
|
||||||
"/home/chris/pics/memes/no-i-dont-think.gif",
|
|
||||||
),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -282,10 +213,10 @@ mod test {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Image,
|
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;
|
image_model.load_from_db(&mut db).await;
|
||||||
if let Some(image) = image_model.find(|i| i.id == 23) {
|
if let Some(image) = image_model.find(|i| i.id == 3) {
|
||||||
let test_image = test_image("no-i-dont-think.gif".into());
|
let test_image = test_image("nccq5".into());
|
||||||
assert_eq!(test_image.title, image.title);
|
assert_eq!(test_image.title, image.title);
|
||||||
} else {
|
} else {
|
||||||
assert!(false);
|
assert!(false);
|
||||||
|
|
@ -319,9 +250,4 @@ mod test {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_db() -> Result<SqlitePool> {
|
|
||||||
let db_url = String::from("sqlite://./test.db");
|
|
||||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
use std::{error::Error, fmt::Display, path::PathBuf};
|
use std::{error::Error, fmt::Display};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::Slide;
|
||||||
Slide,
|
|
||||||
core::{content::Content, service_items::ServiceItem},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
images::Image, presentations::Presentation, songs::Song,
|
images::Image, presentations::Presentation, songs::Song,
|
||||||
|
|
@ -21,67 +18,14 @@ pub enum ServiceItemKind {
|
||||||
Content(Slide),
|
Content(Slide),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<PathBuf> for ServiceItemKind {
|
|
||||||
type Error = miette::Error;
|
|
||||||
|
|
||||||
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
|
||||||
let ext = path
|
|
||||||
.extension()
|
|
||||||
.and_then(|ext| ext.to_str())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
miette::miette!(
|
|
||||||
"There isn't an extension on this file"
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
match ext {
|
|
||||||
"png" | "jpg" | "jpeg" => {
|
|
||||||
Ok(Self::Image(Image::from(path)))
|
|
||||||
}
|
|
||||||
"mp4" | "mkv" | "webm" => {
|
|
||||||
Ok(Self::Video(Video::from(path)))
|
|
||||||
}
|
|
||||||
"pdf" => Ok(Self::Presentation(Presentation::from(path))),
|
|
||||||
_ => Err(miette::miette!("Unknown item")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServiceItemKind {
|
|
||||||
pub fn title(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Song(song) => song.title.clone(),
|
|
||||||
Self::Video(video) => video.title.clone(),
|
|
||||||
Self::Image(image) => image.title.clone(),
|
|
||||||
Self::Presentation(presentation) => {
|
|
||||||
presentation.title.clone()
|
|
||||||
}
|
|
||||||
Self::Content(_slide) => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_service_item(&self) -> ServiceItem {
|
|
||||||
match self {
|
|
||||||
Self::Song(song) => song.to_service_item(),
|
|
||||||
Self::Video(video) => video.to_service_item(),
|
|
||||||
Self::Image(image) => image.to_service_item(),
|
|
||||||
Self::Presentation(presentation) => {
|
|
||||||
presentation.to_service_item()
|
|
||||||
}
|
|
||||||
Self::Content(_slide) => {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ServiceItemKind {
|
impl std::fmt::Display for ServiceItemKind {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
let s = match self {
|
let s = match self {
|
||||||
Self::Song(_) => "song".to_owned(),
|
Self::Song(s) => "song".to_owned(),
|
||||||
Self::Image(_) => "image".to_owned(),
|
Self::Image(i) => "image".to_owned(),
|
||||||
Self::Video(_) => "video".to_owned(),
|
Self::Video(v) => "video".to_owned(),
|
||||||
Self::Presentation(_) => "html".to_owned(),
|
Self::Presentation(p) => "html".to_owned(),
|
||||||
Self::Content(_) => "content".to_owned(),
|
Self::Content(s) => "content".to_owned(),
|
||||||
};
|
};
|
||||||
write!(f, "{s}")
|
write!(f, "{s}")
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +50,7 @@ impl std::fmt::Display for ServiceItemKind {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
impl From<ServiceItemKind> for String {
|
impl From<ServiceItemKind> for String {
|
||||||
fn from(val: ServiceItemKind) -> Self {
|
fn from(val: ServiceItemKind) -> String {
|
||||||
match val {
|
match val {
|
||||||
ServiceItemKind::Song(_) => "song".to_owned(),
|
ServiceItemKind::Song(_) => "song".to_owned(),
|
||||||
ServiceItemKind::Video(_) => "video".to_owned(),
|
ServiceItemKind::Video(_) => "video".to_owned(),
|
||||||
|
|
@ -132,9 +76,7 @@ impl Display for ParseError {
|
||||||
f: &mut std::fmt::Formatter<'_>,
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
) -> std::fmt::Result {
|
) -> std::fmt::Result {
|
||||||
let message = match self {
|
let message = match self {
|
||||||
Self::UnknownType => {
|
Self::UnknownType => "The type does not exist. It needs to be one of 'song', 'video', 'image', 'presentation', or 'content'",
|
||||||
"The type does not exist. It needs to be one of 'song', 'video', 'image', 'presentation', or 'content'"
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
write!(f, "Error: {message}")
|
write!(f, "Error: {message}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
157
src/core/lisp.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
use lexpr::Value;
|
||||||
|
use strum_macros::EnumString;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, EnumString)]
|
||||||
|
pub(crate) enum Symbol {
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Slide,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Image,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Text,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Video,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Song,
|
||||||
|
#[strum(disabled)]
|
||||||
|
ImageFit(ImageFit),
|
||||||
|
#[strum(disabled)]
|
||||||
|
VerseOrder(VerseOrder),
|
||||||
|
#[strum(disabled)]
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, EnumString)]
|
||||||
|
pub(crate) enum Keyword {
|
||||||
|
ImageFit(ImageFit),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, EnumString)]
|
||||||
|
pub(crate) enum ImageFit {
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
#[default]
|
||||||
|
Cover,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Fill,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Crop,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, EnumString)]
|
||||||
|
pub(crate) enum VerseOrder {
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
#[default]
|
||||||
|
V1,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
V2,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
V3,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
V4,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
V5,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
V6,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
C1,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
C2,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
C3,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
C4,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
B1,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
B2,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
B3,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
B4,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
O1,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
O2,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
O3,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
O4,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
E1,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
E2,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
I1,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
I2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, EnumString)]
|
||||||
|
pub(crate) enum SongKeyword {
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Title,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Author,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Ccli,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Audio,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Font,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
FontSize,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Background,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
VerseOrder(VerseOrder),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, EnumString)]
|
||||||
|
pub(crate) enum ImageKeyword {
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Source,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Fit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, EnumString)]
|
||||||
|
pub(crate) enum VideoKeyword {
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Source,
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
Fit,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_lists(exp: &Value) -> Vec<Value> {
|
||||||
|
if exp.is_cons() {
|
||||||
|
exp.as_cons().unwrap().to_vec().0
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn test_list() {
|
||||||
|
// let lisp =
|
||||||
|
// read_to_string("./test_presentation.lisp").expect("oops");
|
||||||
|
// // println!("{lisp}");
|
||||||
|
// let mut parser =
|
||||||
|
// Parser::from_str_custom(&lisp, Options::elisp());
|
||||||
|
// for atom in parser.value_iter() {
|
||||||
|
// match atom {
|
||||||
|
// Ok(atom) => {
|
||||||
|
// // println!("{atom}");
|
||||||
|
// let lists = get_lists(&atom);
|
||||||
|
// assert_eq!(lists, vec![Value::Null])
|
||||||
|
// }
|
||||||
|
// Err(e) => {
|
||||||
|
// panic!("{e}");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
pub mod content;
|
pub mod content;
|
||||||
pub mod file;
|
|
||||||
pub mod images;
|
pub mod images;
|
||||||
pub mod kinds;
|
pub mod kinds;
|
||||||
|
pub mod lisp;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod presentations;
|
pub mod presentations;
|
||||||
pub mod service_items;
|
pub mod service_items;
|
||||||
pub mod settings;
|
|
||||||
pub mod slide;
|
pub mod slide;
|
||||||
pub mod slide_actions;
|
|
||||||
pub mod song_search;
|
|
||||||
pub mod songs;
|
pub mod songs;
|
||||||
pub mod thumbnail;
|
pub mod thumbnail;
|
||||||
pub mod videos;
|
pub mod videos;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
use std::{borrow::Cow, fs, mem::replace, path::PathBuf};
|
use std::mem::replace;
|
||||||
|
|
||||||
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
use miette::{miette, Result};
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{Connection, SqliteConnection};
|
use sqlx::{Connection, SqliteConnection};
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Model<T> {
|
pub struct Model<T> {
|
||||||
|
|
@ -12,9 +9,7 @@ pub struct Model<T> {
|
||||||
pub kind: LibraryKind,
|
pub kind: LibraryKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||||
Debug, Clone, PartialEq, Eq, Copy, Hash, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub enum LibraryKind {
|
pub enum LibraryKind {
|
||||||
Song,
|
Song,
|
||||||
Video,
|
Video,
|
||||||
|
|
@ -22,98 +17,36 @@ pub enum LibraryKind {
|
||||||
Presentation,
|
Presentation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct KindWrapper(pub (LibraryKind, i32));
|
|
||||||
|
|
||||||
impl From<PathBuf> for LibraryKind {
|
|
||||||
fn from(_value: PathBuf) -> Self {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<(Vec<u8>, String)> for KindWrapper {
|
|
||||||
type Error = miette::Error;
|
|
||||||
|
|
||||||
fn try_from(
|
|
||||||
value: (Vec<u8>, String),
|
|
||||||
) -> std::result::Result<Self, Self::Error> {
|
|
||||||
let (data, mime) = value;
|
|
||||||
match mime.as_str() {
|
|
||||||
"application/service-item" => {
|
|
||||||
ron::de::from_bytes(&data).into_diagnostic()
|
|
||||||
}
|
|
||||||
_ => Err(miette!("Wrong mime type: {mime}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AllowedMimeTypes for KindWrapper {
|
|
||||||
fn allowed() -> Cow<'static, [String]> {
|
|
||||||
Cow::from(vec!["application/service-item".to_string()])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsMimeTypes for KindWrapper {
|
|
||||||
fn available(&self) -> Cow<'static, [String]> {
|
|
||||||
debug!(?self);
|
|
||||||
Cow::from(vec!["application/service-item".to_string()])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_bytes(
|
|
||||||
&self,
|
|
||||||
mime_type: &str,
|
|
||||||
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
|
||||||
debug!(?self);
|
|
||||||
debug!(mime_type);
|
|
||||||
let ron = ron::ser::to_string(self).ok()?;
|
|
||||||
debug!(ron);
|
|
||||||
Some(Cow::from(ron.into_bytes()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Model<T> {
|
impl<T> Model<T> {
|
||||||
pub fn add_item(&mut self, item: T) -> Result<()> {
|
pub fn add_item(&mut self, item: T) -> Result<()> {
|
||||||
self.items.push(item);
|
self.items.push(item);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_to_db(&mut self, _item: T) -> Result<()> {
|
pub fn add_to_db(&mut self, item: T) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_item(&mut self, item: T, index: i32) -> Result<()> {
|
pub fn update_item(&mut self, item: T, index: i32) -> Result<()> {
|
||||||
self.items
|
if let Some(current_item) = self.items.get_mut(index as usize)
|
||||||
.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);
|
let _old_item = replace(current_item, item);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
} else {
|
||||||
)
|
Err(miette!(
|
||||||
|
"Item doesn't exist in model. Id was {}",
|
||||||
|
index
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_item(&mut self, index: i32) -> Result<()> {
|
pub fn remove_item(&mut self, index: i32) -> Result<()> {
|
||||||
self.items.remove(
|
self.items.remove(index as usize);
|
||||||
usize::try_from(index).expect("Shouldn't be negative"),
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn get_item(&self, index: i32) -> Option<&T> {
|
pub fn get_item(&self, index: i32) -> Option<&T> {
|
||||||
self.items.get(
|
self.items.get(index as usize)
|
||||||
usize::try_from(index).expect("shouldn't be negative"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find<P>(&self, f: P) -> Option<&T>
|
pub fn find<P>(&self, f: P) -> Option<&T>
|
||||||
|
|
@ -124,10 +57,7 @@ impl<T> Model<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_item(&mut self, item: T, index: i32) -> Result<()> {
|
pub fn insert_item(&mut self, item: T, index: i32) -> Result<()> {
|
||||||
self.items.insert(
|
self.items.insert(index as usize, item);
|
||||||
usize::try_from(index).expect("Shouldn't be negative"),
|
|
||||||
item,
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -144,13 +74,11 @@ impl<T> Model<T> {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub async fn get_db() -> SqliteConnection {
|
pub async fn get_db() -> SqliteConnection {
|
||||||
let mut data = dirs::data_local_dir()
|
let mut data = dirs::data_local_dir().unwrap();
|
||||||
.expect("Should be able to find a data dir");
|
|
||||||
data.push("lumina");
|
data.push("lumina");
|
||||||
let _ = fs::create_dir_all(&data);
|
|
||||||
data.push("library-db.sqlite3");
|
data.push("library-db.sqlite3");
|
||||||
let mut db_url = String::from("sqlite://");
|
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")
|
SqliteConnection::connect(&db_url).await.expect("problems")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
use cosmic::widget::image::Handle;
|
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use mupdf::{Colorspace, Document, Matrix};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
Row, Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
|
pool::PoolConnection, prelude::FromRow, query, sqlite::SqliteRow,
|
||||||
prelude::FromRow, query, sqlite::SqliteRow,
|
Row, Sqlite, SqliteConnection, SqlitePool,
|
||||||
};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use tracing::{debug, error};
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{Background, Slide, SlideBuilder, TextAlignment};
|
use crate::{Background, Slide, SlideBuilder, TextAlignment};
|
||||||
|
|
||||||
|
|
@ -24,15 +22,14 @@ use super::{
|
||||||
)]
|
)]
|
||||||
pub enum PresKind {
|
pub enum PresKind {
|
||||||
Html,
|
Html,
|
||||||
Pdf {
|
|
||||||
starting_index: i32,
|
|
||||||
ending_index: i32,
|
|
||||||
},
|
|
||||||
#[default]
|
#[default]
|
||||||
|
Pdf,
|
||||||
Generic,
|
Generic,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(
|
||||||
|
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
pub struct Presentation {
|
pub struct Presentation {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
@ -40,69 +37,8 @@ pub struct Presentation {
|
||||||
pub kind: PresKind,
|
pub kind: PresKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for Presentation {}
|
|
||||||
|
|
||||||
impl PartialEq for Presentation {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.id == other.id
|
|
||||||
&& self.title == other.title
|
|
||||||
&& self.path == other.path
|
|
||||||
&& self.kind == other.kind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PathBuf> for Presentation {
|
|
||||||
fn from(value: PathBuf) -> Self {
|
|
||||||
let title = value
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_str()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string();
|
|
||||||
let kind = match value
|
|
||||||
.extension()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.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(
|
|
||||||
PresKind::Pdf {
|
|
||||||
starting_index: 0,
|
|
||||||
ending_index: 0,
|
|
||||||
},
|
|
||||||
|count| PresKind::Pdf {
|
|
||||||
starting_index: 0,
|
|
||||||
ending_index: count - 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"html" => PresKind::Html,
|
|
||||||
_ => PresKind::Generic,
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
id: 0,
|
|
||||||
title,
|
|
||||||
path: value.canonicalize().unwrap_or(value),
|
|
||||||
kind,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Path> for Presentation {
|
|
||||||
fn from(value: &Path) -> Self {
|
|
||||||
Self::from(value.to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Presentation> for Value {
|
impl From<&Presentation> for Value {
|
||||||
fn from(_value: &Presentation) -> Self {
|
fn from(value: &Presentation) -> Self {
|
||||||
Self::List(vec![Self::Symbol(Symbol("presentation".into()))])
|
Self::List(vec![Self::Symbol(Symbol("presentation".into()))])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,10 +62,10 @@ impl Content for Presentation {
|
||||||
|
|
||||||
fn subtext(&self) -> String {
|
fn subtext(&self) -> String {
|
||||||
if self.path.exists() {
|
if self.path.exists() {
|
||||||
self.path.file_name().map_or_else(
|
self.path
|
||||||
|| "Missing presentation".into(),
|
.file_name()
|
||||||
|f| f.to_string_lossy().to_string(),
|
.map(|f| f.to_string_lossy().to_string())
|
||||||
)
|
.unwrap_or("Missing presentation".into())
|
||||||
} else {
|
} else {
|
||||||
"Missing presentation".into()
|
"Missing presentation".into()
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +78,6 @@ impl From<Value> for Presentation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::option_if_let_else)]
|
|
||||||
impl From<&Value> for Presentation {
|
impl From<&Value> for Presentation {
|
||||||
fn from(value: &Value) -> Self {
|
fn from(value: &Value) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
@ -182,55 +117,6 @@ impl ServiceTrait for Presentation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_slides(&self) -> Result<Vec<Slide>> {
|
fn to_slides(&self) -> Result<Vec<Slide>> {
|
||||||
debug!(?self);
|
|
||||||
let PresKind::Pdf {
|
|
||||||
starting_index,
|
|
||||||
ending_index,
|
|
||||||
} = self.kind
|
|
||||||
else {
|
|
||||||
return Err(miette::miette!(
|
|
||||||
"This is not a pdf presentation"
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let background = Background::try_from(self.path.clone())
|
|
||||||
.into_diagnostic()?;
|
|
||||||
debug!(?background);
|
|
||||||
let document = Document::open(background.path.as_path())
|
|
||||||
.into_diagnostic()?;
|
|
||||||
debug!(?document);
|
|
||||||
let pages = document.pages().into_diagnostic()?;
|
|
||||||
debug!(?pages);
|
|
||||||
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 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let page = page.ok()?;
|
|
||||||
let matrix = Matrix::IDENTITY;
|
|
||||||
let colorspace = Colorspace::device_rgb();
|
|
||||||
let Ok(pixmap) = page
|
|
||||||
.to_pixmap(&matrix, &colorspace, true, true)
|
|
||||||
.into_diagnostic()
|
|
||||||
else {
|
|
||||||
error!("Can't turn this page into pixmap");
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
debug!(?pixmap);
|
|
||||||
Some(Handle::from_rgba(
|
|
||||||
pixmap.width(),
|
|
||||||
pixmap.height(),
|
|
||||||
pixmap.samples().to_vec(),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut slides: Vec<Slide> = vec![];
|
|
||||||
for (index, page) in pages.into_iter().enumerate() {
|
|
||||||
let slide = SlideBuilder::new()
|
let slide = SlideBuilder::new()
|
||||||
.background(
|
.background(
|
||||||
Background::try_from(self.path.clone())
|
Background::try_from(self.path.clone())
|
||||||
|
|
@ -244,16 +130,9 @@ impl ServiceTrait for Presentation {
|
||||||
.video_loop(false)
|
.video_loop(false)
|
||||||
.video_start_time(0.0)
|
.video_start_time(0.0)
|
||||||
.video_end_time(0.0)
|
.video_end_time(0.0)
|
||||||
.pdf_index(
|
|
||||||
u32::try_from(index)
|
|
||||||
.expect("Shouldn't get that high"),
|
|
||||||
)
|
|
||||||
.pdf_page(page)
|
|
||||||
.build()?;
|
.build()?;
|
||||||
slides.push(slide);
|
|
||||||
}
|
Ok(vec![slide])
|
||||||
debug!(?slides);
|
|
||||||
Ok(slides)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn box_clone(&self) -> Box<dyn ServiceTrait> {
|
fn box_clone(&self) -> Box<dyn ServiceTrait> {
|
||||||
|
|
@ -262,16 +141,14 @@ impl ServiceTrait for Presentation {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Presentation {
|
impl Presentation {
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
title: String::new(),
|
title: "".to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn get_kind(&self) -> &PresKind {
|
||||||
pub const fn get_kind(&self) -> &PresKind {
|
|
||||||
&self.kind
|
&self.kind
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -288,10 +165,7 @@ impl FromRow<'_, SqliteRow> for Presentation {
|
||||||
kind: if row.try_get(3)? {
|
kind: if row.try_get(3)? {
|
||||||
PresKind::Html
|
PresKind::Html
|
||||||
} else {
|
} else {
|
||||||
PresKind::Pdf {
|
PresKind::Pdf
|
||||||
starting_index: row.try_get(4)?,
|
|
||||||
ending_index: row.try_get(5)?,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -311,60 +185,21 @@ impl Model<Presentation> {
|
||||||
|
|
||||||
pub async fn load_from_db(&mut self, db: &mut SqliteConnection) {
|
pub async fn load_from_db(&mut self, db: &mut SqliteConnection) {
|
||||||
let result = query!(
|
let result = query!(
|
||||||
r#"SELECT id as "id: i32", title, file_path as "path", html, starting_index, ending_index from presentations"#
|
r#"SELECT id as "id: i32", title, file_path as "path", html from presentations"#
|
||||||
)
|
)
|
||||||
.fetch_all(db)
|
.fetch_all(db)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
for presentation in v {
|
for presentation in v.into_iter() {
|
||||||
let _ = self.add_item(Presentation {
|
let _ = self.add_item(Presentation {
|
||||||
id: presentation.id,
|
id: presentation.id,
|
||||||
title: presentation.title,
|
title: presentation.title,
|
||||||
path: presentation.path.clone().into(),
|
path: presentation.path.into(),
|
||||||
kind: if presentation.html {
|
kind: if presentation.html {
|
||||||
PresKind::Html
|
PresKind::Html
|
||||||
} else if let (
|
|
||||||
Some(starting_index),
|
|
||||||
Some(ending_index),
|
|
||||||
) = (
|
|
||||||
presentation.starting_index,
|
|
||||||
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"),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let path =
|
PresKind::Pdf
|
||||||
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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -376,51 +211,6 @@ impl Model<Presentation> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_from_db(
|
|
||||||
db: PoolConnection<Sqlite>,
|
|
||||||
id: i32,
|
|
||||||
) -> Result<()> {
|
|
||||||
query!("DELETE FROM presentations WHERE id = $1", id)
|
|
||||||
.execute(&mut db.detach())
|
|
||||||
.await
|
|
||||||
.into_diagnostic()
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_presentation_to_db(
|
|
||||||
presentation: Presentation,
|
|
||||||
db: PoolConnection<Sqlite>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let path = presentation
|
|
||||||
.path
|
|
||||||
.to_str()
|
|
||||||
.map(std::string::ToString::to_string)
|
|
||||||
.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)"#,
|
|
||||||
presentation.title,
|
|
||||||
path,
|
|
||||||
html,
|
|
||||||
starting_index,
|
|
||||||
ending_index
|
|
||||||
)
|
|
||||||
.execute(&mut db)
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_presentation_in_db(
|
pub async fn update_presentation_in_db(
|
||||||
presentation: Presentation,
|
presentation: Presentation,
|
||||||
db: PoolConnection<Sqlite>,
|
db: PoolConnection<Sqlite>,
|
||||||
|
|
@ -428,90 +218,22 @@ pub async fn update_presentation_in_db(
|
||||||
let path = presentation
|
let path = presentation
|
||||||
.path
|
.path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(std::string::ToString::to_string)
|
.map(|s| s.to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let html = presentation.kind == PresKind::Html;
|
let html = presentation.kind == PresKind::Html;
|
||||||
let mut db = db.detach();
|
query!(
|
||||||
let (starting_index, ending_index) = if let PresKind::Pdf {
|
r#"UPDATE presentations SET title = $2, file_path = $3, html = $4 WHERE id = $1"#,
|
||||||
starting_index: s_index,
|
|
||||||
ending_index: e_index,
|
|
||||||
} =
|
|
||||||
presentation.get_kind()
|
|
||||||
{
|
|
||||||
(*s_index, *e_index)
|
|
||||||
} else {
|
|
||||||
(0, 0)
|
|
||||||
};
|
|
||||||
debug!(starting_index, ending_index);
|
|
||||||
let id = presentation.id;
|
|
||||||
if let Err(e) =
|
|
||||||
query!("SELECT id FROM presentations where id = $1", id)
|
|
||||||
.fetch_one(&mut db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if let Ok(ids) = query!("SELECT id FROM presentations")
|
|
||||||
.fetch_all(&mut db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
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"
|
|
||||||
);
|
|
||||||
max += 1;
|
|
||||||
let result = query!(
|
|
||||||
r#"INSERT into presentations VALUES($1, $2, $3, $4, $5, $6)"#,
|
|
||||||
max,
|
|
||||||
presentation.title,
|
|
||||||
path,
|
|
||||||
html,
|
|
||||||
starting_index,
|
|
||||||
ending_index,
|
|
||||||
)
|
|
||||||
.execute(&mut db)
|
|
||||||
.await
|
|
||||||
.into_diagnostic();
|
|
||||||
|
|
||||||
return match result {
|
|
||||||
Ok(_) => {
|
|
||||||
debug!("presentation should have been added");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error! {?e};
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return Err(miette::miette!("cannot find ids"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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"#,
|
|
||||||
presentation.id,
|
presentation.id,
|
||||||
presentation.title,
|
presentation.title,
|
||||||
path,
|
path,
|
||||||
html,
|
html
|
||||||
starting_index,
|
|
||||||
ending_index
|
|
||||||
)
|
)
|
||||||
.execute(&mut db)
|
.execute(&mut db.detach())
|
||||||
.await.into_diagnostic();
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => {
|
|
||||||
debug!("should have been updated");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
error! {?e};
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_presentation_from_db(
|
pub async fn get_presentation_from_db(
|
||||||
database_id: i32,
|
database_id: i32,
|
||||||
|
|
@ -528,25 +250,19 @@ mod test {
|
||||||
|
|
||||||
fn test_presentation() -> Presentation {
|
fn test_presentation() -> Presentation {
|
||||||
Presentation {
|
Presentation {
|
||||||
id: 4,
|
id: 54,
|
||||||
title: "mzt52.pdf".into(),
|
title: "20240327T133649--12-isaiah-and-jesus__lesson_project_tfc".into(),
|
||||||
path: PathBuf::from("/home/chris/docs/mzt52.pdf"),
|
path: PathBuf::from(
|
||||||
kind: PresKind::Pdf {
|
"file:///home/chris/docs/notes/lessons/20240327T133649--12-isaiah-and-jesus__lesson_project_tfc.html",
|
||||||
starting_index: 0,
|
),
|
||||||
ending_index: 67,
|
kind: PresKind::Html,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_pres() {
|
pub fn test_pres() {
|
||||||
let pres = Presentation::new();
|
let pres = Presentation::new();
|
||||||
assert_eq!(pres.get_kind(), &PresKind::Generic)
|
assert_eq!(pres.get_kind(), &PresKind::Pdf)
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_db() -> Result<SqlitePool> {
|
|
||||||
let mut db_url = String::from("sqlite://./test.db");
|
|
||||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -555,10 +271,10 @@ mod test {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Presentation,
|
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;
|
presentation_model.load_from_db(&mut db).await;
|
||||||
if let Some(presentation) =
|
if let Some(presentation) =
|
||||||
presentation_model.find(|p| p.id == 4)
|
presentation_model.find(|p| p.id == 54)
|
||||||
{
|
{
|
||||||
let test_presentation = test_presentation();
|
let test_presentation = test_presentation();
|
||||||
assert_eq!(&test_presentation, presentation);
|
assert_eq!(&test_presentation, presentation);
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,29 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
// use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
||||||
use serde::{Deserialize, Serialize};
|
use miette::Result;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::Slide;
|
use crate::Slide;
|
||||||
|
|
||||||
use super::images::Image;
|
use super::images::Image;
|
||||||
use super::presentations::Presentation;
|
use super::presentations::Presentation;
|
||||||
use super::songs::{Song, lisp_to_song};
|
use super::songs::{lisp_to_song, Song};
|
||||||
use super::videos::Video;
|
use super::videos::Video;
|
||||||
|
|
||||||
use super::kinds::ServiceItemKind;
|
use super::kinds::ServiceItemKind;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct ServiceItem {
|
pub struct ServiceItem {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub database_id: i32,
|
pub database_id: i32,
|
||||||
pub kind: ServiceItemKind,
|
pub kind: ServiceItemKind,
|
||||||
pub slides: Vec<Slide>,
|
pub slides: Arc<[Slide]>,
|
||||||
// pub item: Box<dyn ServiceTrait>,
|
// pub item: Box<dyn ServiceTrait>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,7 +31,7 @@ impl Eq for ServiceItem {}
|
||||||
|
|
||||||
impl PartialOrd for ServiceItem {
|
impl PartialOrd for ServiceItem {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
self.id.partial_cmp(&other.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,74 +47,49 @@ impl TryFrom<(Vec<u8>, String)> for ServiceItem {
|
||||||
fn try_from(
|
fn try_from(
|
||||||
value: (Vec<u8>, String),
|
value: (Vec<u8>, String),
|
||||||
) -> std::result::Result<Self, Self::Error> {
|
) -> std::result::Result<Self, Self::Error> {
|
||||||
let (data, mime) = value;
|
debug!(?value);
|
||||||
debug!(?mime);
|
let val = Value::from(
|
||||||
ron::de::from_bytes(&data).into_diagnostic()
|
String::from_utf8(value.0)
|
||||||
|
.expect("Value couldn't be made"),
|
||||||
|
);
|
||||||
|
Ok(Self::from(&val))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AllowedMimeTypes for ServiceItem {
|
// impl AllowedMimeTypes for ServiceItem {
|
||||||
fn allowed() -> Cow<'static, [String]> {
|
// fn allowed() -> Cow<'static, [String]> {
|
||||||
Cow::from(vec![
|
// Cow::from(vec!["application/service-item".to_string()])
|
||||||
"application/service-item".to_string(),
|
// }
|
||||||
"text/uri-list".to_string(),
|
// }
|
||||||
"x-special/gnome-copied-files".to_string(),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsMimeTypes for ServiceItem {
|
// impl AsMimeTypes for ServiceItem {
|
||||||
fn available(&self) -> Cow<'static, [String]> {
|
// fn available(&self) -> Cow<'static, [String]> {
|
||||||
debug!(?self);
|
// debug!(?self);
|
||||||
Cow::from(vec!["application/service-item".to_string()])
|
// Cow::from(vec!["application/service-item".to_string()])
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn as_bytes(
|
// fn as_bytes(
|
||||||
&self,
|
// &self,
|
||||||
mime_type: &str,
|
// mime_type: &str,
|
||||||
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
// ) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||||
debug!(?self);
|
// debug!(?self);
|
||||||
debug!(mime_type);
|
// debug!(mime_type);
|
||||||
let ron = ron::ser::to_string(self).ok()?;
|
// let val = Value::from(self);
|
||||||
debug!(ron);
|
// let val = String::from(val);
|
||||||
Some(Cow::from(ron.into_bytes()))
|
// Some(Cow::from(val.into_bytes()))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl TryFrom<PathBuf> for ServiceItem {
|
|
||||||
type Error = miette::Error;
|
|
||||||
|
|
||||||
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
|
||||||
let ext = path
|
|
||||||
.extension()
|
|
||||||
.and_then(|ext| ext.to_str())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
miette::miette!(
|
|
||||||
"There isn't an extension on this file"
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
match ext {
|
|
||||||
"png" | "jpg" | "jpeg" => {
|
|
||||||
Ok(Self::from(&Image::from(path)))
|
|
||||||
}
|
|
||||||
"mp4" | "mkv" | "webm" => {
|
|
||||||
Ok(Self::from(&Video::from(path)))
|
|
||||||
}
|
|
||||||
_ => Err(miette!("Unkown service item")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&ServiceItem> for Value {
|
impl From<&ServiceItem> for Value {
|
||||||
fn from(value: &ServiceItem) -> Self {
|
fn from(value: &ServiceItem) -> Self {
|
||||||
match &value.kind {
|
match &value.kind {
|
||||||
ServiceItemKind::Song(song) => Self::from(song),
|
ServiceItemKind::Song(song) => Value::from(song),
|
||||||
ServiceItemKind::Video(video) => Self::from(video),
|
ServiceItemKind::Video(video) => Value::from(video),
|
||||||
ServiceItemKind::Image(image) => Self::from(image),
|
ServiceItemKind::Image(image) => Value::from(image),
|
||||||
ServiceItemKind::Presentation(presentation) => {
|
ServiceItemKind::Presentation(presentation) => {
|
||||||
Self::from(presentation)
|
Value::from(presentation)
|
||||||
}
|
}
|
||||||
ServiceItemKind::Content(slide) => Self::from(slide),
|
ServiceItemKind::Content(slide) => Value::from(slide),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +121,7 @@ impl Default for ServiceItem {
|
||||||
title: String::default(),
|
title: String::default(),
|
||||||
database_id: 0,
|
database_id: 0,
|
||||||
kind: ServiceItemKind::Content(Slide::default()),
|
kind: ServiceItemKind::Content(Slide::default()),
|
||||||
slides: vec![],
|
slides: Arc::new([]),
|
||||||
// item: Box::new(Image::default()),
|
// item: Box::new(Image::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -159,8 +133,6 @@ impl From<Value> for ServiceItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::option_if_let_else)]
|
|
||||||
#[allow(clippy::match_like_matches_macro)]
|
|
||||||
impl From<&Value> for ServiceItem {
|
impl From<&Value> for ServiceItem {
|
||||||
fn from(value: &Value) -> Self {
|
fn from(value: &Value) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
@ -177,7 +149,7 @@ impl From<&Value> for ServiceItem {
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
.map_or_else(|| 1, |pos| pos + 1);
|
.map_or_else(|| 1, |pos| pos + 1);
|
||||||
if let Some(_content) =
|
if let Some(content) =
|
||||||
list.iter().position(|v| match v {
|
list.iter().position(|v| match v {
|
||||||
Value::List(list)
|
Value::List(list)
|
||||||
if list.iter().next()
|
if list.iter().next()
|
||||||
|
|
@ -199,13 +171,13 @@ impl From<&Value> for ServiceItem {
|
||||||
kind: ServiceItemKind::Content(
|
kind: ServiceItemKind::Content(
|
||||||
slide.clone(),
|
slide.clone(),
|
||||||
),
|
),
|
||||||
slides: vec![slide],
|
slides: Arc::new([slide]),
|
||||||
}
|
}
|
||||||
} else if let Some(background) =
|
} else if let Some(background) =
|
||||||
list.get(background_pos)
|
list.get(background_pos)
|
||||||
{
|
{
|
||||||
if let Value::List(item) = background {
|
match background {
|
||||||
match &item[0] {
|
Value::List(item) => match &item[0] {
|
||||||
Value::Symbol(Symbol(s))
|
Value::Symbol(Symbol(s))
|
||||||
if s == "image" =>
|
if s == "image" =>
|
||||||
{
|
{
|
||||||
|
|
@ -228,29 +200,30 @@ impl From<&Value> for ServiceItem {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
},
|
||||||
} else {
|
_ => {
|
||||||
error!(
|
error!(
|
||||||
"There is no background here: {:?}",
|
"There is no background here: {:?}",
|
||||||
background
|
background
|
||||||
);
|
);
|
||||||
Self::default()
|
ServiceItem::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
"There is no background here: {:?}",
|
"There is no background here: {:?}",
|
||||||
background_pos
|
background_pos
|
||||||
);
|
);
|
||||||
Self::default()
|
ServiceItem::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::Symbol(Symbol(s)) if s == "song" => {
|
Value::Symbol(Symbol(s)) if s == "song" => {
|
||||||
let song = lisp_to_song(list.clone());
|
let song = lisp_to_song(list.clone());
|
||||||
Self::from(&song)
|
Self::from(&song)
|
||||||
}
|
}
|
||||||
_ => Self::default(),
|
_ => ServiceItem::default(),
|
||||||
},
|
},
|
||||||
_ => Self::default(),
|
_ => ServiceItem::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -284,78 +257,80 @@ impl From<Vec<ServiceItem>> for Service {
|
||||||
|
|
||||||
impl From<&Song> for ServiceItem {
|
impl From<&Song> for ServiceItem {
|
||||||
fn from(song: &Song) -> Self {
|
fn from(song: &Song) -> Self {
|
||||||
song.to_slides().map_or_else(
|
if let Ok(slides) = song.to_slides() {
|
||||||
|_| Self {
|
Self {
|
||||||
|
kind: ServiceItemKind::Song(song.clone()),
|
||||||
|
database_id: song.id,
|
||||||
|
title: song.title.clone(),
|
||||||
|
slides: slides.into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
kind: ServiceItemKind::Song(song.clone()),
|
kind: ServiceItemKind::Song(song.clone()),
|
||||||
database_id: song.id,
|
database_id: song.id,
|
||||||
title: song.title.clone(),
|
title: song.title.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
}
|
||||||
|slides| Self {
|
}
|
||||||
kind: ServiceItemKind::Song(song.clone()),
|
|
||||||
database_id: song.id,
|
|
||||||
title: song.title.clone(),
|
|
||||||
slides,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Video> for ServiceItem {
|
impl From<&Video> for ServiceItem {
|
||||||
fn from(video: &Video) -> Self {
|
fn from(video: &Video) -> Self {
|
||||||
video.to_slides().map_or_else(
|
if let Ok(slides) = video.to_slides() {
|
||||||
|_| Self {
|
Self {
|
||||||
|
kind: ServiceItemKind::Video(video.clone()),
|
||||||
|
database_id: video.id,
|
||||||
|
title: video.title.clone(),
|
||||||
|
slides: slides.into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
kind: ServiceItemKind::Video(video.clone()),
|
kind: ServiceItemKind::Video(video.clone()),
|
||||||
database_id: video.id,
|
database_id: video.id,
|
||||||
title: video.title.clone(),
|
title: video.title.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
}
|
||||||
|slides| Self {
|
}
|
||||||
kind: ServiceItemKind::Video(video.clone()),
|
|
||||||
database_id: video.id,
|
|
||||||
title: video.title.clone(),
|
|
||||||
slides,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Image> for ServiceItem {
|
impl From<&Image> for ServiceItem {
|
||||||
fn from(image: &Image) -> Self {
|
fn from(image: &Image) -> Self {
|
||||||
image.to_slides().map_or_else(
|
if let Ok(slides) = image.to_slides() {
|
||||||
|_| Self {
|
Self {
|
||||||
|
kind: ServiceItemKind::Image(image.clone()),
|
||||||
|
database_id: image.id,
|
||||||
|
title: image.title.clone(),
|
||||||
|
slides: slides.into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
kind: ServiceItemKind::Image(image.clone()),
|
kind: ServiceItemKind::Image(image.clone()),
|
||||||
database_id: image.id,
|
database_id: image.id,
|
||||||
title: image.title.clone(),
|
title: image.title.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
}
|
||||||
|slides| Self {
|
}
|
||||||
kind: ServiceItemKind::Image(image.clone()),
|
|
||||||
database_id: image.id,
|
|
||||||
title: image.title.clone(),
|
|
||||||
slides,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Presentation> for ServiceItem {
|
impl From<&Presentation> for ServiceItem {
|
||||||
fn from(presentation: &Presentation) -> Self {
|
fn from(presentation: &Presentation) -> Self {
|
||||||
match presentation.to_slides() {
|
if let Ok(slides) = presentation.to_slides() {
|
||||||
Ok(slides) => Self {
|
Self {
|
||||||
kind: ServiceItemKind::Presentation(
|
kind: ServiceItemKind::Presentation(
|
||||||
presentation.clone(),
|
presentation.clone(),
|
||||||
),
|
),
|
||||||
database_id: presentation.id,
|
database_id: presentation.id,
|
||||||
title: presentation.title.clone(),
|
title: presentation.title.clone(),
|
||||||
slides,
|
slides: slides.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
}
|
||||||
Err(e) => {
|
} else {
|
||||||
error!(?e);
|
|
||||||
Self {
|
Self {
|
||||||
kind: ServiceItemKind::Presentation(
|
kind: ServiceItemKind::Presentation(
|
||||||
presentation.clone(),
|
presentation.clone(),
|
||||||
|
|
@ -367,13 +342,15 @@ impl From<&Presentation> for ServiceItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
impl Service {
|
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();
|
let service_item: ServiceItem = item.into();
|
||||||
self.items.push(service_item);
|
self.items.push(service_item);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_slides(&self) -> Result<Vec<Slide>> {
|
pub fn to_slides(&self) -> Result<Vec<Slide>> {
|
||||||
|
|
@ -389,7 +366,7 @@ impl Service {
|
||||||
.collect::<Vec<Slide>>();
|
.collect::<Vec<Slide>>();
|
||||||
let mut final_slides = vec![];
|
let mut final_slides = vec![];
|
||||||
for (index, mut slide) in slides.into_iter().enumerate() {
|
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);
|
final_slides.push(slide);
|
||||||
}
|
}
|
||||||
Ok(final_slides)
|
Ok(final_slides)
|
||||||
|
|
@ -452,7 +429,8 @@ mod test {
|
||||||
let pres = test_presentation();
|
let pres = test_presentation();
|
||||||
let pres_item = ServiceItem::from(&pres);
|
let pres_item = ServiceItem::from(&pres);
|
||||||
let mut service_model = Service::default();
|
let mut service_model = Service::default();
|
||||||
service_model.add_item(&song);
|
match service_model.add_item(&song) {
|
||||||
|
Ok(_) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ServiceItemKind::Song(song),
|
ServiceItemKind::Song(song),
|
||||||
service_model.items[0].kind
|
service_model.items[0].kind
|
||||||
|
|
@ -463,4 +441,7 @@ mod test {
|
||||||
);
|
);
|
||||||
assert_eq!(service_item, service_model.items[0]);
|
assert_eq!(service_item, service_model.items[0]);
|
||||||
}
|
}
|
||||||
|
Err(e) => panic!("Problem adding item: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
use cosmic::{
|
|
||||||
cosmic_config::{
|
|
||||||
self, CosmicConfigEntry,
|
|
||||||
cosmic_config_derive::CosmicConfigEntry,
|
|
||||||
},
|
|
||||||
theme,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{collections::VecDeque, path::PathBuf};
|
|
||||||
|
|
||||||
pub const SETTINGS_VERSION: u64 = 1;
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize,
|
|
||||||
)]
|
|
||||||
pub enum AppTheme {
|
|
||||||
Dark,
|
|
||||||
Light,
|
|
||||||
System,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppTheme {
|
|
||||||
pub fn theme(&self) -> theme::Theme {
|
|
||||||
match self {
|
|
||||||
Self::Dark => theme::Theme::dark(),
|
|
||||||
Self::Light => theme::Theme::light(),
|
|
||||||
Self::System => theme::system_preference(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone,
|
|
||||||
CosmicConfigEntry,
|
|
||||||
Debug,
|
|
||||||
Deserialize,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
Serialize,
|
|
||||||
)]
|
|
||||||
#[serde(default)]
|
|
||||||
pub struct Settings {
|
|
||||||
pub app_theme: AppTheme,
|
|
||||||
pub obs_url: Option<url::Url>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Settings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
app_theme: AppTheme::System,
|
|
||||||
obs_url: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone,
|
|
||||||
CosmicConfigEntry,
|
|
||||||
Debug,
|
|
||||||
Deserialize,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
Serialize,
|
|
||||||
Default,
|
|
||||||
)]
|
|
||||||
pub struct PersistentState {
|
|
||||||
pub recent_files: VecDeque<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
#![allow(clippy::similar_names, unused)]
|
// use iced::dialog::ashpd::url::Url;
|
||||||
use cosmic::widget::image::Handle;
|
|
||||||
// use cosmic::dialog::ashpd::url::Url;
|
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use iced_video_player::Video;
|
use iced_video_player::Video;
|
||||||
use miette::{Result, miette};
|
use miette::{miette, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
|
|
@ -11,44 +9,10 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::ui::text_svg::{Color, Font, Shadow, Stroke, TextSvg};
|
use crate::ui::text_svg::{self, TextSvg};
|
||||||
|
|
||||||
use super::songs::Song;
|
use super::songs::Song;
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Slide {
|
|
||||||
id: i32,
|
|
||||||
pub(crate) background: Background,
|
|
||||||
text: String,
|
|
||||||
font: Option<Font>,
|
|
||||||
font_size: i32,
|
|
||||||
stroke: Option<Stroke>,
|
|
||||||
shadow: Option<Shadow>,
|
|
||||||
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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub enum BackgroundKind {
|
|
||||||
#[default]
|
|
||||||
Image,
|
|
||||||
Video,
|
|
||||||
Pdf,
|
|
||||||
Html,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone,
|
Clone,
|
||||||
Copy,
|
Copy,
|
||||||
|
|
@ -103,9 +67,9 @@ impl TryFrom<&Background> for Video {
|
||||||
fn try_from(
|
fn try_from(
|
||||||
value: &Background,
|
value: &Background,
|
||||||
) -> std::result::Result<Self, Self::Error> {
|
) -> std::result::Result<Self, Self::Error> {
|
||||||
Self::new(
|
Video::new(
|
||||||
&url::Url::from_file_path(value.path.clone())
|
&url::Url::from_file_path(value.path.clone())
|
||||||
.map_err(|()| ParseError::BackgroundNotVideo)?,
|
.map_err(|_| ParseError::BackgroundNotVideo)?,
|
||||||
)
|
)
|
||||||
.map_err(|_| ParseError::BackgroundNotVideo)
|
.map_err(|_| ParseError::BackgroundNotVideo)
|
||||||
}
|
}
|
||||||
|
|
@ -117,9 +81,9 @@ impl TryFrom<Background> for Video {
|
||||||
fn try_from(
|
fn try_from(
|
||||||
value: Background,
|
value: Background,
|
||||||
) -> std::result::Result<Self, Self::Error> {
|
) -> std::result::Result<Self, Self::Error> {
|
||||||
Self::new(
|
Video::new(
|
||||||
&url::Url::from_file_path(value.path)
|
&url::Url::from_file_path(value.path)
|
||||||
.map_err(|()| ParseError::BackgroundNotVideo)?,
|
.map_err(|_| ParseError::BackgroundNotVideo)?,
|
||||||
)
|
)
|
||||||
.map_err(|_| ParseError::BackgroundNotVideo)
|
.map_err(|_| ParseError::BackgroundNotVideo)
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +92,7 @@ impl TryFrom<Background> for Video {
|
||||||
impl TryFrom<String> for Background {
|
impl TryFrom<String> for Background {
|
||||||
type Error = ParseError;
|
type Error = ParseError;
|
||||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
Self::try_from(value.as_str())
|
Background::try_from(value.as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,17 +100,14 @@ impl TryFrom<PathBuf> for Background {
|
||||||
type Error = ParseError;
|
type Error = ParseError;
|
||||||
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
||||||
let path = if path.starts_with("~") {
|
let path = if path.starts_with("~") {
|
||||||
let path = path
|
let path = path.to_str().unwrap().to_string();
|
||||||
.to_str()
|
|
||||||
.expect("Should have a string")
|
|
||||||
.to_string();
|
|
||||||
let path = path.trim_start_matches("file://");
|
let path = path.trim_start_matches("file://");
|
||||||
let home = dirs::home_dir()
|
let home = dirs::home_dir()
|
||||||
.expect("We should have a home directory")
|
.unwrap()
|
||||||
.to_str()
|
.to_str()
|
||||||
.expect("Gah")
|
.unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
let path = path.replace('~', &home);
|
let path = path.replace("~", &home);
|
||||||
PathBuf::from(path)
|
PathBuf::from(path)
|
||||||
} else {
|
} else {
|
||||||
path
|
path
|
||||||
|
|
@ -160,27 +121,21 @@ impl TryFrom<PathBuf> for Background {
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
match extension {
|
match extension {
|
||||||
"jpeg" | "jpg" | "png" | "webp" => Ok(Self {
|
"jpeg" | "jpg" | "png" | "webp" | "html" => {
|
||||||
|
Ok(Self {
|
||||||
path: value,
|
path: value,
|
||||||
kind: BackgroundKind::Image,
|
kind: BackgroundKind::Image,
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
"mp4" | "mkv" | "webm" => Ok(Self {
|
"mp4" | "mkv" | "webm" => Ok(Self {
|
||||||
path: value,
|
path: value,
|
||||||
kind: BackgroundKind::Video,
|
kind: BackgroundKind::Video,
|
||||||
}),
|
}),
|
||||||
"pdf" => Ok(Self {
|
|
||||||
path: value,
|
|
||||||
kind: BackgroundKind::Pdf,
|
|
||||||
}),
|
|
||||||
"html" => Ok(Self {
|
|
||||||
path: value,
|
|
||||||
kind: BackgroundKind::Html,
|
|
||||||
}),
|
|
||||||
_ => Err(ParseError::NonBackgroundFile),
|
_ => Err(ParseError::NonBackgroundFile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_e) => {
|
Err(e) => {
|
||||||
// error!("Couldn't canonicalize: {e} {:?}", path);
|
error!("Couldn't canonicalize: {e} {:?}", path);
|
||||||
Err(ParseError::CannotCanonicalize)
|
Err(ParseError::CannotCanonicalize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -191,19 +146,17 @@ impl TryFrom<&str> for Background {
|
||||||
type Error = ParseError;
|
type Error = ParseError;
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
let value = value.trim_start_matches("file://");
|
let value = value.trim_start_matches("file://");
|
||||||
if value.starts_with('~') {
|
if value.starts_with("~") {
|
||||||
dirs::home_dir().map_or_else(
|
if let Some(home) = dirs::home_dir() {
|
||||||
|| Self::try_from(PathBuf::from(value)),
|
if let Some(home) = home.to_str() {
|
||||||
|home| {
|
let value = value.replace("~", 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))
|
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("./") {
|
} else if value.starts_with("./") {
|
||||||
Err(ParseError::CannotCanonicalize)
|
Err(ParseError::CannotCanonicalize)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -250,140 +203,97 @@ impl Display for ParseError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub enum BackgroundKind {
|
||||||
|
#[default]
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
}
|
||||||
|
|
||||||
impl From<String> for BackgroundKind {
|
impl From<String> for BackgroundKind {
|
||||||
fn from(value: String) -> Self {
|
fn from(value: String) -> Self {
|
||||||
if value == "image" {
|
if value == "image" {
|
||||||
Self::Image
|
BackgroundKind::Image
|
||||||
} else {
|
} else {
|
||||||
Self::Video
|
BackgroundKind::Video
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct Slide {
|
||||||
|
id: i32,
|
||||||
|
background: Background,
|
||||||
|
text: String,
|
||||||
|
font: String,
|
||||||
|
font_size: i32,
|
||||||
|
text_alignment: TextAlignment,
|
||||||
|
audio: Option<PathBuf>,
|
||||||
|
video_loop: bool,
|
||||||
|
video_start_time: f32,
|
||||||
|
video_end_time: f32,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub text_svg: TextSvg,
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&Slide> for Value {
|
impl From<&Slide> for Value {
|
||||||
fn from(_value: &Slide) -> Self {
|
fn from(value: &Slide) -> Self {
|
||||||
Self::List(vec![Self::Symbol(Symbol("slide".into()))])
|
Self::List(vec![Self::Symbol(Symbol("slide".into()))])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Slide {
|
impl Slide {
|
||||||
#[must_use]
|
|
||||||
pub fn set_text(mut self, text: impl AsRef<str>) -> Self {
|
pub fn set_text(mut self, text: impl AsRef<str>) -> Self {
|
||||||
self.text = text.as_ref().into();
|
self.text = text.as_ref().into();
|
||||||
self
|
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 {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn set_font_size(mut self, font_size: i32) -> Self {
|
||||||
pub const fn set_font_size(mut self, font_size: i32) -> Self {
|
|
||||||
self.font_size = font_size;
|
self.font_size = font_size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn set_audio(mut self, audio: Option<PathBuf>) -> Self {
|
pub fn set_audio(mut self, audio: Option<PathBuf>) -> Self {
|
||||||
self.audio = audio;
|
self.audio = audio;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn background(&self) -> &Background {
|
||||||
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
|
&self.background
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn text(&self) -> String {
|
pub fn text(&self) -> String {
|
||||||
self.text.clone()
|
self.text.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn font_size(&self) -> i32 {
|
||||||
pub const fn text_alignment(&self) -> TextAlignment {
|
|
||||||
self.text_alignment
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn font_size(&self) -> i32 {
|
|
||||||
self.font_size
|
self.font_size
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn font(&self) -> String {
|
||||||
pub fn font(&self) -> Option<Font> {
|
|
||||||
self.font.clone()
|
self.font.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn video_loop(&self) -> bool {
|
||||||
pub const fn video_loop(&self) -> bool {
|
|
||||||
self.video_loop
|
self.video_loop
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn audio(&self) -> Option<PathBuf> {
|
pub fn audio(&self) -> Option<PathBuf> {
|
||||||
self.audio.clone()
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn song_slides(song: &Song) -> Result<Vec<Self>> {
|
pub fn song_slides(song: &Song) -> Result<Vec<Self>> {
|
||||||
let lyrics = song.get_lyrics()?;
|
let lyrics = song.get_lyrics()?;
|
||||||
let slides: Vec<Self> = lyrics
|
let slides: Vec<Slide> = lyrics
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| {
|
.map(|l| {
|
||||||
let song = song.clone();
|
let song = song.clone();
|
||||||
|
|
@ -407,10 +317,14 @@ impl Slide {
|
||||||
Ok(slides)
|
Ok(slides)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const fn set_index(&mut self, index: i32) {
|
pub(crate) fn set_index(&mut self, index: i32) {
|
||||||
self.id = index;
|
self.id = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn text_to_image(&self) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
// pub fn slides_from_item(item: &ServiceItem) -> Result<Vec<Self>> {
|
// pub fn slides_from_item(item: &ServiceItem) -> Result<Vec<Self>> {
|
||||||
// todo!()
|
// todo!()
|
||||||
// }
|
// }
|
||||||
|
|
@ -426,13 +340,12 @@ impl From<&Value> for Slide {
|
||||||
fn from(value: &Value) -> Self {
|
fn from(value: &Value) -> Self {
|
||||||
match value {
|
match value {
|
||||||
Value::List(list) => lisp_to_slide(list),
|
Value::List(list) => lisp_to_slide(list),
|
||||||
_ => Self::default(),
|
_ => Slide::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::option_if_let_else)]
|
fn lisp_to_slide(lisp: &Vec<Value>) -> Slide {
|
||||||
fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
|
||||||
const DEFAULT_BACKGROUND_LOCATION: usize = 1;
|
const DEFAULT_BACKGROUND_LOCATION: usize = 1;
|
||||||
const DEFAULT_TEXT_LOCATION: usize = 0;
|
const DEFAULT_TEXT_LOCATION: usize = 0;
|
||||||
|
|
||||||
|
|
@ -450,7 +363,7 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
||||||
slide = slide.background(lisp_to_background(background));
|
slide = slide.background(lisp_to_background(background));
|
||||||
} else {
|
} else {
|
||||||
slide = slide.background(Background::default());
|
slide = slide.background(Background::default());
|
||||||
}
|
};
|
||||||
|
|
||||||
let text_position = lisp.iter().position(|v| match v {
|
let text_position = lisp.iter().position(|v| match v {
|
||||||
Value::List(vec) => {
|
Value::List(vec) => {
|
||||||
|
|
@ -496,7 +409,6 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::option_if_let_else)]
|
|
||||||
fn lisp_to_font_size(lisp: &Value) -> i32 {
|
fn lisp_to_font_size(lisp: &Value) -> i32 {
|
||||||
match lisp {
|
match lisp {
|
||||||
Value::List(list) => {
|
Value::List(list) => {
|
||||||
|
|
@ -529,11 +441,10 @@ fn lisp_to_text(lisp: &Value) -> impl Into<String> {
|
||||||
|
|
||||||
// Need to return a Result here so that we can propogate
|
// Need to return a Result here so that we can propogate
|
||||||
// errors and then handle them appropriately
|
// errors and then handle them appropriately
|
||||||
#[allow(clippy::option_if_let_else)]
|
|
||||||
pub fn lisp_to_background(lisp: &Value) -> Background {
|
pub fn lisp_to_background(lisp: &Value) -> Background {
|
||||||
match lisp {
|
match lisp {
|
||||||
Value::List(list) => {
|
Value::List(list) => {
|
||||||
let _kind = list[0].clone();
|
let kind = list[0].clone();
|
||||||
if let Some(source) = list.iter().position(|v| {
|
if let Some(source) = list.iter().position(|v| {
|
||||||
v == &Value::Keyword(Keyword::from("source"))
|
v == &Value::Keyword(Keyword::from("source"))
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -588,19 +499,13 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
||||||
pub struct SlideBuilder {
|
pub struct SlideBuilder {
|
||||||
background: Option<Background>,
|
background: Option<Background>,
|
||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
font: Option<Font>,
|
font: Option<String>,
|
||||||
font_size: Option<i32>,
|
font_size: Option<i32>,
|
||||||
audio: Option<PathBuf>,
|
audio: Option<PathBuf>,
|
||||||
stroke: Option<Stroke>,
|
|
||||||
shadow: Option<Shadow>,
|
|
||||||
text_color: Option<Color>,
|
|
||||||
text_alignment: Option<TextAlignment>,
|
text_alignment: Option<TextAlignment>,
|
||||||
video_loop: Option<bool>,
|
video_loop: Option<bool>,
|
||||||
video_start_time: Option<f32>,
|
video_start_time: Option<f32>,
|
||||||
video_end_time: Option<f32>,
|
video_end_time: Option<f32>,
|
||||||
pdf_index: Option<u32>,
|
|
||||||
#[serde(skip)]
|
|
||||||
pdf_page: Option<Handle>,
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
text_svg: Option<TextSvg>,
|
text_svg: Option<TextSvg>,
|
||||||
}
|
}
|
||||||
|
|
@ -632,20 +537,12 @@ impl SlideBuilder {
|
||||||
self
|
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 {
|
pub(crate) fn audio(mut self, audio: impl Into<PathBuf>) -> Self {
|
||||||
let _ = self.audio.insert(audio.into());
|
let _ = self.audio.insert(audio.into());
|
||||||
self
|
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());
|
let _ = self.font.insert(font.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -655,27 +552,6 @@ impl SlideBuilder {
|
||||||
self
|
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(
|
pub(crate) fn text_alignment(
|
||||||
mut self,
|
mut self,
|
||||||
text_alignment: TextAlignment,
|
text_alignment: TextAlignment,
|
||||||
|
|
@ -713,19 +589,6 @@ impl SlideBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pdf_page(mut self, pdf_page: Handle) -> Self {
|
|
||||||
let _ = self.pdf_page.insert(pdf_page);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn pdf_index(
|
|
||||||
mut self,
|
|
||||||
pdf_index: impl Into<u32>,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.pdf_index.insert(pdf_index.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build(self) -> Result<Slide> {
|
pub(crate) fn build(self) -> Result<Slide> {
|
||||||
let Some(background) = self.background else {
|
let Some(background) = self.background else {
|
||||||
return Err(miette!("No background"));
|
return Err(miette!("No background"));
|
||||||
|
|
@ -733,6 +596,9 @@ impl SlideBuilder {
|
||||||
let Some(text) = self.text else {
|
let Some(text) = self.text else {
|
||||||
return Err(miette!("No text"));
|
return Err(miette!("No text"));
|
||||||
};
|
};
|
||||||
|
let Some(font) = self.font else {
|
||||||
|
return Err(miette!("No font"));
|
||||||
|
};
|
||||||
let Some(font_size) = self.font_size else {
|
let Some(font_size) = self.font_size else {
|
||||||
return Err(miette!("No font_size"));
|
return Err(miette!("No font_size"));
|
||||||
};
|
};
|
||||||
|
|
@ -748,24 +614,60 @@ impl SlideBuilder {
|
||||||
let Some(video_end_time) = self.video_end_time else {
|
let Some(video_end_time) = self.video_end_time else {
|
||||||
return Err(miette!("No video_end_time"));
|
return Err(miette!("No video_end_time"));
|
||||||
};
|
};
|
||||||
|
if let Some(text_svg) = self.text_svg {
|
||||||
Ok(Slide {
|
Ok(Slide {
|
||||||
background,
|
background,
|
||||||
text,
|
text,
|
||||||
font: self.font,
|
font,
|
||||||
font_size,
|
font_size,
|
||||||
text_alignment,
|
text_alignment,
|
||||||
audio: self.audio,
|
audio: self.audio,
|
||||||
stroke: self.stroke,
|
|
||||||
shadow: self.shadow,
|
|
||||||
text_color: self.text_color,
|
|
||||||
video_loop,
|
video_loop,
|
||||||
video_start_time,
|
video_start_time,
|
||||||
video_end_time,
|
video_end_time,
|
||||||
text_svg: self.text_svg,
|
text_svg,
|
||||||
pdf_index: self.pdf_index.unwrap_or_default(),
|
|
||||||
pdf_page: self.pdf_page,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
let text_svg = TextSvg::new(text.clone())
|
||||||
|
.alignment(text_alignment)
|
||||||
|
.fill("#fff")
|
||||||
|
.shadow(text_svg::shadow(2, 2, 5, "#000000"))
|
||||||
|
.stroke(text_svg::stroke(3, "#000"))
|
||||||
|
.font(
|
||||||
|
text_svg::Font::from(font.clone())
|
||||||
|
.size(font_size.try_into().unwrap()),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
Ok(Slide {
|
||||||
|
background,
|
||||||
|
text,
|
||||||
|
font,
|
||||||
|
font_size,
|
||||||
|
text_alignment,
|
||||||
|
audio: self.audio,
|
||||||
|
video_loop,
|
||||||
|
video_start_time,
|
||||||
|
video_end_time,
|
||||||
|
text_svg,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct Image {
|
||||||
|
pub source: String,
|
||||||
|
pub fit: String,
|
||||||
|
pub children: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -781,8 +683,8 @@ mod test {
|
||||||
text: "This is frodo".to_string(),
|
text: "This is frodo".to_string(),
|
||||||
background: Background::try_from("~/pics/frodo.jpg")
|
background: Background::try_from("~/pics/frodo.jpg")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
font: Some("Quicksand".to_string().into()),
|
font: "Quicksand".to_string(),
|
||||||
font_size: 140,
|
font_size: 70,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -794,21 +696,40 @@ mod test {
|
||||||
"~/vids/test/camprules2024.mp4",
|
"~/vids/test/camprules2024.mp4",
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
font: Some("Quicksand".to_string().into()),
|
font: "Quicksand".to_string(),
|
||||||
..Default::default()
|
..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]
|
#[test]
|
||||||
fn test_ron_deserialize() {
|
fn test_ron_deserialize() {
|
||||||
let slide = read_to_string("./test_presentation.ron")
|
let slide = read_to_string("./test_presentation.ron")
|
||||||
.expect("Problem getting file read");
|
.expect("Problem getting file read");
|
||||||
match ron::from_str::<Vec<Slide>>(&slide) {
|
match ron::from_str::<Vec<Slide>>(&slide) {
|
||||||
Ok(_s) => {
|
Ok(s) => {
|
||||||
assert!(true)
|
assert!(true)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
assert!(false, "{:?}", e)
|
assert!(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
use miette::{IntoDiagnostic, Result};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use obws::{Client, responses::scenes::Scene};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub(crate) enum ObsAction {
|
|
||||||
Scene { scene: Scene },
|
|
||||||
StartStream,
|
|
||||||
StopStream,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub(crate) enum Action {
|
|
||||||
Obs { action: ObsAction },
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObsAction {
|
|
||||||
pub async fn run(&self, client: Arc<Client>) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
Self::Scene { scene } => {
|
|
||||||
warn!(?scene, "Changing obs scenes");
|
|
||||||
client
|
|
||||||
.scenes()
|
|
||||||
.set_current_program_scene(&scene.id)
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?;
|
|
||||||
}
|
|
||||||
Self::StartStream => {
|
|
||||||
client.streaming().start().await.into_diagnostic()?;
|
|
||||||
}
|
|
||||||
Self::StopStream => {
|
|
||||||
client.streaming().stop().await.into_diagnostic()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,327 +0,0 @@
|
||||||
use itertools::Itertools;
|
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
|
||||||
use reqwest::header;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone,
|
|
||||||
Debug,
|
|
||||||
Default,
|
|
||||||
PartialEq,
|
|
||||||
PartialOrd,
|
|
||||||
Ord,
|
|
||||||
Eq,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct OnlineSong {
|
|
||||||
pub lyrics: String,
|
|
||||||
pub title: String,
|
|
||||||
pub author: String,
|
|
||||||
pub site: String,
|
|
||||||
pub 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(
|
|
||||||
query: impl AsRef<str> + std::fmt::Display,
|
|
||||||
) -> Result<Vec<String>> {
|
|
||||||
let html =
|
|
||||||
reqwest::get(format!("http://www.lyrics.com/lyrics/{query}"))
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?
|
|
||||||
.error_for_status()
|
|
||||||
.into_diagnostic()?
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.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"));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(document
|
|
||||||
.select(&best_matches_selector)
|
|
||||||
.flat_map(|best_section| best_section.select(&lyric_selector))
|
|
||||||
.map(|a| {
|
|
||||||
a.value().attr("href").unwrap_or("").trim().to_string()
|
|
||||||
})
|
|
||||||
.filter(|a| a.contains("/lyric/"))
|
|
||||||
.dedup()
|
|
||||||
.map(|link| {
|
|
||||||
link.strip_prefix("/lyric/")
|
|
||||||
.unwrap_or_else(|| &link)
|
|
||||||
.to_string()
|
|
||||||
})
|
|
||||||
.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(
|
|
||||||
links: Vec<impl AsRef<str> + std::fmt::Display>,
|
|
||||||
) -> Result<Vec<OnlineSong>> {
|
|
||||||
let mut songs = vec![];
|
|
||||||
for link in links {
|
|
||||||
let parts = link
|
|
||||||
.as_ref()
|
|
||||||
.split('/')
|
|
||||||
.map(std::string::ToString::to_string)
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
let link = format!("https://www.lyrics.com/lyric/{link}");
|
|
||||||
let _id = &parts[0];
|
|
||||||
let author = &parts[1].replace('+', " ");
|
|
||||||
let title = &parts[2].replace('+', " ");
|
|
||||||
|
|
||||||
let html = reqwest::get(&link)
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?
|
|
||||||
.error_for_status()
|
|
||||||
.into_diagnostic()?
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.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 lyrics = document
|
|
||||||
.select(&lyric_selector)
|
|
||||||
.map(|a| a.text().collect::<String>())
|
|
||||||
.dedup()
|
|
||||||
.next();
|
|
||||||
|
|
||||||
if let Some(lyrics) = lyrics {
|
|
||||||
let song = OnlineSong {
|
|
||||||
lyrics,
|
|
||||||
title: title.clone(),
|
|
||||||
author: author.clone(),
|
|
||||||
site: "https://www.lyrics.com".into(),
|
|
||||||
link,
|
|
||||||
};
|
|
||||||
|
|
||||||
songs.push(song);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(songs)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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> {
|
|
||||||
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(),
|
|
||||||
author: "North Point InsideOut".to_string(),
|
|
||||||
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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(String::from(
|
|
||||||
"There is no VerseMap in this song",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_online_search() {
|
|
||||||
let search =
|
|
||||||
search_lyrics_com_links("Death was arrested").await;
|
|
||||||
match search {
|
|
||||||
Ok(songs) => {
|
|
||||||
assert_eq!(
|
|
||||||
songs,
|
|
||||||
vec![
|
|
||||||
"33755723/Various+Artists/Death+Was+Arrested",
|
|
||||||
"35090938/North+Point+InsideOut/Death+Was+Arrested"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(e) => assert!(false, "{}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1277
src/core/songs.rs
|
|
@ -11,13 +11,12 @@ pub fn bg_from_video(
|
||||||
video: &Path,
|
video: &Path,
|
||||||
screenshot: &Path,
|
screenshot: &Path,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
if screenshot.exists() {
|
if !screenshot.exists() {
|
||||||
debug!("Screenshot already exists");
|
|
||||||
} else {
|
|
||||||
let output_duration = Command::new("ffprobe")
|
let output_duration = Command::new("ffprobe")
|
||||||
.args(["-i", &video.to_string_lossy()])
|
.args(["-i", &video.to_string_lossy()])
|
||||||
.output()?;
|
.output()
|
||||||
io::stderr().write_all(&output_duration.stderr)?;
|
.expect("failed to execute ffprobe");
|
||||||
|
io::stderr().write_all(&output_duration.stderr).unwrap();
|
||||||
let mut at_second = 5;
|
let mut at_second = 5;
|
||||||
let mut log = str::from_utf8(&output_duration.stderr)
|
let mut log = str::from_utf8(&output_duration.stderr)
|
||||||
.expect("Using non UTF-8 characters")
|
.expect("Using non UTF-8 characters")
|
||||||
|
|
@ -27,9 +26,9 @@ pub fn bg_from_video(
|
||||||
let mut duration = log.split_off(duration_index + 10);
|
let mut duration = log.split_off(duration_index + 10);
|
||||||
duration.truncate(11);
|
duration.truncate(11);
|
||||||
// debug!("rust-duration-is: {duration}");
|
// debug!("rust-duration-is: {duration}");
|
||||||
let mut hours = String::new();
|
let mut hours = String::from("");
|
||||||
let mut minutes = String::new();
|
let mut minutes = String::from("");
|
||||||
let mut seconds = String::new();
|
let mut seconds = String::from("");
|
||||||
for (i, c) in duration.chars().enumerate() {
|
for (i, c) in duration.chars().enumerate() {
|
||||||
if i <= 1 {
|
if i <= 1 {
|
||||||
hours.push(c);
|
hours.push(c);
|
||||||
|
|
@ -64,6 +63,8 @@ pub fn bg_from_video(
|
||||||
.expect("failed to execute ffmpeg");
|
.expect("failed to execute ffmpeg");
|
||||||
// io::stdout().write_all(&output.stdout).unwrap();
|
// io::stdout().write_all(&output.stdout).unwrap();
|
||||||
// io::stderr().write_all(&output.stderr).unwrap();
|
// io::stderr().write_all(&output.stderr).unwrap();
|
||||||
|
} else {
|
||||||
|
debug!("Screenshot already exists");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -71,18 +72,15 @@ pub fn bg_from_video(
|
||||||
pub fn bg_path_from_video(video: &Path) -> PathBuf {
|
pub fn bg_path_from_video(video: &Path) -> PathBuf {
|
||||||
let video = PathBuf::from(video);
|
let video = PathBuf::from(video);
|
||||||
debug!(?video);
|
debug!(?video);
|
||||||
let mut data_dir =
|
let mut data_dir = dirs::data_local_dir().unwrap();
|
||||||
dirs::cache_dir().expect("Can't find cache dir");
|
|
||||||
data_dir.push("lumina");
|
data_dir.push("lumina");
|
||||||
data_dir.push("thumbnails");
|
data_dir.push("thumbnails");
|
||||||
let _ = fs::create_dir_all(&data_dir);
|
|
||||||
if !data_dir.exists() {
|
if !data_dir.exists() {
|
||||||
fs::create_dir(&data_dir)
|
fs::create_dir(&data_dir)
|
||||||
.expect("Could not create thumbnails dir");
|
.expect("Could not create thumbnails dir");
|
||||||
}
|
}
|
||||||
let mut screenshot = data_dir.clone();
|
let mut screenshot = data_dir.clone();
|
||||||
screenshot
|
screenshot.push(video.file_name().unwrap());
|
||||||
.push(video.file_name().expect("Should have file name"));
|
|
||||||
screenshot.set_extension("png");
|
screenshot.set_extension("png");
|
||||||
screenshot
|
screenshot
|
||||||
}
|
}
|
||||||
|
|
@ -93,9 +91,16 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bg_video_creation() {
|
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);
|
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()),
|
Ok(_o) => assert!(screenshot.exists()),
|
||||||
Err(e) => debug_assert!(
|
Err(e) => debug_assert!(
|
||||||
false,
|
false,
|
||||||
|
|
@ -104,4 +109,15 @@ 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use miette::{IntoDiagnostic, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
|
pool::PoolConnection, query, query_as, Sqlite, SqliteConnection,
|
||||||
query, query_as,
|
SqlitePool,
|
||||||
};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use tracing::{debug, error};
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
||||||
|
|
@ -30,31 +30,11 @@ pub struct Video {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Video> for Value {
|
impl From<&Video> for Value {
|
||||||
fn from(_value: &Video) -> Self {
|
fn from(value: &Video) -> Self {
|
||||||
Self::List(vec![Self::Symbol(Symbol("video".into()))])
|
Self::List(vec![Self::Symbol(Symbol("video".into()))])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PathBuf> for Video {
|
|
||||||
fn from(value: PathBuf) -> Self {
|
|
||||||
let title: String = value.file_name().map_or_else(
|
|
||||||
|| "Video".into(),
|
|
||||||
|filename| filename.to_str().unwrap_or("Video").into(),
|
|
||||||
);
|
|
||||||
Self {
|
|
||||||
title,
|
|
||||||
path: value,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Path> for Video {
|
|
||||||
fn from(value: &Path) -> Self {
|
|
||||||
Self::from(value.to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Content for Video {
|
impl Content for Video {
|
||||||
fn title(&self) -> String {
|
fn title(&self) -> String {
|
||||||
self.title.clone()
|
self.title.clone()
|
||||||
|
|
@ -74,10 +54,10 @@ impl Content for Video {
|
||||||
|
|
||||||
fn subtext(&self) -> String {
|
fn subtext(&self) -> String {
|
||||||
if self.path.exists() {
|
if self.path.exists() {
|
||||||
self.path.file_name().map_or_else(
|
self.path
|
||||||
|| "Missing video".into(),
|
.file_name()
|
||||||
|f| f.to_string_lossy().to_string(),
|
.map(|f| f.to_string_lossy().to_string())
|
||||||
)
|
.unwrap_or("Missing video".into())
|
||||||
} else {
|
} else {
|
||||||
"Missing video".into()
|
"Missing video".into()
|
||||||
}
|
}
|
||||||
|
|
@ -90,65 +70,64 @@ impl From<Value> for Video {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
impl From<&Value> for Video {
|
impl From<&Value> for Video {
|
||||||
fn from(value: &Value) -> Self {
|
fn from(value: &Value) -> Self {
|
||||||
match value {
|
match value {
|
||||||
Value::List(list) => {
|
Value::List(list) => {
|
||||||
let path = list
|
let path = if let Some(path_pos) =
|
||||||
.iter()
|
list.iter().position(|v| {
|
||||||
.position(|v| {
|
|
||||||
v == &Value::Keyword(Keyword::from("source"))
|
v == &Value::Keyword(Keyword::from("source"))
|
||||||
})
|
}) {
|
||||||
.and_then(|path_pos| {
|
|
||||||
let pos = path_pos + 1;
|
let pos = path_pos + 1;
|
||||||
list.get(pos)
|
list.get(pos)
|
||||||
.map(|p| PathBuf::from(String::from(p)))
|
.map(|p| PathBuf::from(String::from(p)))
|
||||||
});
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let title = path.clone().map(|p| {
|
let title = path.clone().map(|p| {
|
||||||
let path =
|
let path =
|
||||||
p.to_str().unwrap_or_default().to_string();
|
p.to_str().unwrap_or_default().to_string();
|
||||||
let title =
|
let title =
|
||||||
path.rsplit_once('/').unwrap_or_default().1;
|
path.rsplit_once("/").unwrap_or_default().1;
|
||||||
title.to_string()
|
title.to_string()
|
||||||
});
|
});
|
||||||
|
|
||||||
let start_time = list
|
let start_time = if let Some(start_pos) =
|
||||||
.iter()
|
list.iter().position(|v| {
|
||||||
.position(|v| {
|
|
||||||
v == &Value::Keyword(Keyword::from(
|
v == &Value::Keyword(Keyword::from(
|
||||||
"start-time",
|
"start-time",
|
||||||
))
|
))
|
||||||
})
|
}) {
|
||||||
.and_then(|start_pos| {
|
|
||||||
let pos = start_pos + 1;
|
let pos = start_pos + 1;
|
||||||
list.get(pos).map(|p| i32::from(p) as f32)
|
list.get(pos).map(|p| i32::from(p) as f32)
|
||||||
});
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let end_time = list
|
let end_time = if let Some(end_pos) =
|
||||||
.iter()
|
list.iter().position(|v| {
|
||||||
.position(|v| {
|
|
||||||
v == &Value::Keyword(Keyword::from(
|
v == &Value::Keyword(Keyword::from(
|
||||||
"end-time",
|
"end-time",
|
||||||
))
|
))
|
||||||
})
|
}) {
|
||||||
.and_then(|end_pos| {
|
|
||||||
let pos = end_pos + 1;
|
let pos = end_pos + 1;
|
||||||
list.get(pos).map(|p| i32::from(p) as f32)
|
list.get(pos).map(|p| i32::from(p) as f32)
|
||||||
});
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let looping = list
|
let looping = if let Some(loop_pos) =
|
||||||
.iter()
|
list.iter().position(|v| {
|
||||||
.position(|v| {
|
|
||||||
v == &Value::Keyword(Keyword::from("loop"))
|
v == &Value::Keyword(Keyword::from("loop"))
|
||||||
})
|
}) {
|
||||||
.is_some_and(|loop_pos| {
|
|
||||||
let pos = loop_pos + 1;
|
let pos = loop_pos + 1;
|
||||||
list.get(pos).is_some_and(|l| {
|
list.get(pos)
|
||||||
String::from(l) == *"true"
|
.map(|l| String::from(l) == *"true")
|
||||||
})
|
.unwrap_or_default()
|
||||||
});
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
title: title.unwrap_or_default(),
|
title: title.unwrap_or_default(),
|
||||||
|
|
@ -213,53 +192,16 @@ impl Model<Video> {
|
||||||
let result = query_as!(Video, r#"SELECT title as "title!", file_path as "path!", start_time as "start_time!: f32", end_time as "end_time!: f32", loop as "looping!", id as "id: i32" from videos"#).fetch_all(db).await;
|
let result = query_as!(Video, r#"SELECT title as "title!", file_path as "path!", start_time as "start_time!: f32", end_time as "end_time!: f32", loop as "looping!", id as "id: i32" from videos"#).fetch_all(db).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
for video in v {
|
for video in v.into_iter() {
|
||||||
let _ = self.add_item(video);
|
let _ = self.add_item(video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!("There was an error in converting videos: {e}")
|
||||||
"There was an error in converting videos: {e}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove_from_db(
|
|
||||||
db: PoolConnection<Sqlite>,
|
|
||||||
id: i32,
|
|
||||||
) -> Result<()> {
|
|
||||||
query!("DELETE FROM videos WHERE id = $1", id)
|
|
||||||
.execute(&mut db.detach())
|
|
||||||
.await
|
|
||||||
.into_diagnostic()
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_video_to_db(
|
|
||||||
video: Video,
|
|
||||||
db: PoolConnection<Sqlite>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let path = video
|
|
||||||
.path
|
|
||||||
.to_str()
|
|
||||||
.map(std::string::ToString::to_string)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let mut db = db.detach();
|
|
||||||
query!(
|
|
||||||
r#"INSERT INTO videos (title, file_path, start_time, end_time, loop) VALUES ($1, $2, $3, $4, $5)"#,
|
|
||||||
video.title,
|
|
||||||
path,
|
|
||||||
video.start_time,
|
|
||||||
video.end_time,
|
|
||||||
video.looping
|
|
||||||
)
|
|
||||||
.execute(&mut db)
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_video_in_db(
|
pub async fn update_video_in_db(
|
||||||
video: Video,
|
video: Video,
|
||||||
|
|
@ -268,11 +210,9 @@ pub async fn update_video_in_db(
|
||||||
let path = video
|
let path = video
|
||||||
.path
|
.path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(std::string::ToString::to_string)
|
.map(|s| s.to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mut db = db.detach();
|
query!(
|
||||||
debug!(?video, "should be been updated");
|
|
||||||
let result = query!(
|
|
||||||
r#"UPDATE videos SET title = $2, file_path = $3, start_time = $4, end_time = $5, loop = $6 WHERE id = $1"#,
|
r#"UPDATE videos SET title = $2, file_path = $3, start_time = $4, end_time = $5, loop = $6 WHERE id = $1"#,
|
||||||
video.id,
|
video.id,
|
||||||
video.title,
|
video.title,
|
||||||
|
|
@ -281,20 +221,12 @@ pub async fn update_video_in_db(
|
||||||
video.end_time,
|
video.end_time,
|
||||||
video.looping,
|
video.looping,
|
||||||
)
|
)
|
||||||
.execute(&mut db)
|
.execute(&mut db.detach())
|
||||||
.await.into_diagnostic();
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => {
|
|
||||||
debug!("should have been updated");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
error! {?e};
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_video_from_db(
|
pub async fn get_video_from_db(
|
||||||
database_id: i32,
|
database_id: i32,
|
||||||
|
|
@ -311,9 +243,7 @@ mod test {
|
||||||
fn test_video(title: String) -> Video {
|
fn test_video(title: String) -> Video {
|
||||||
Video {
|
Video {
|
||||||
title,
|
title,
|
||||||
path: PathBuf::from(
|
path: PathBuf::from("~/vids/camprules2024.mp4"),
|
||||||
"/home/chris/docs/notes/lessons/christ-our-hope.mp4",
|
|
||||||
),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -324,10 +254,13 @@ mod test {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Video,
|
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;
|
video_model.load_from_db(&mut db).await;
|
||||||
if let Some(video) = video_model.find(|v| v.id == 2) {
|
if let Some(video) = video_model.find(|v| v.id == 73) {
|
||||||
let test_video = test_video("christ-our-hope.mp4".into());
|
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);
|
assert_eq!(test_video.title, video.title);
|
||||||
} else {
|
} else {
|
||||||
assert!(false);
|
assert!(false);
|
||||||
|
|
@ -361,9 +294,4 @@ mod test {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_db() -> Result<SqlitePool> {
|
|
||||||
let db_url = String::from("sqlite://./test.db");
|
|
||||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
275
src/lisp.rs
|
|
@ -35,157 +35,142 @@ pub fn parse_lisp(value: Value) -> Vec<ServiceItem> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(test)]
|
#[cfg(test)]
|
||||||
// mod test {
|
mod test {
|
||||||
// use std::{fs::read_to_string, path::PathBuf};
|
use std::{fs::read_to_string, path::PathBuf};
|
||||||
|
|
||||||
// use crate::core::{
|
use crate::{
|
||||||
// images::Image,
|
core::{
|
||||||
// kinds::ServiceItemKind,
|
images::Image, kinds::ServiceItemKind,
|
||||||
// service_items::ServiceTrait,
|
service_items::ServiceTrait, songs::Song, videos::Video,
|
||||||
// slide::{Background, TextAlignment},
|
},
|
||||||
// songs::Song,
|
Background, TextAlignment,
|
||||||
// videos::Video,
|
};
|
||||||
// };
|
|
||||||
|
|
||||||
// use super::*;
|
use super::*;
|
||||||
// use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
// #[test]
|
#[test]
|
||||||
// fn test_parsing_lisp() {
|
fn test_parsing_lisp() {
|
||||||
// let lisp =
|
let lisp =
|
||||||
// read_to_string("./test_slides.lisp").expect("oops");
|
read_to_string("./test_slides.lisp").expect("oops");
|
||||||
// let lisp_value = crisp::reader::read(&lisp);
|
let lisp_value = crisp::reader::read(&lisp);
|
||||||
// let hard_coded_items =
|
let hard_coded_items =
|
||||||
// vec![service_item_1(), service_item_2()];
|
vec![service_item_1(), service_item_2()];
|
||||||
// match lisp_value {
|
match lisp_value {
|
||||||
// Value::List(value) => {
|
Value::List(value) => {
|
||||||
// let mut lisp_items = vec![];
|
let mut lisp_items = vec![];
|
||||||
// for value in value {
|
for value in value {
|
||||||
// let mut vec = parse_lisp(value);
|
let mut vec = parse_lisp(value);
|
||||||
// lisp_items.append(&mut vec);
|
lisp_items.append(&mut vec);
|
||||||
// }
|
}
|
||||||
// assert_eq!(lisp_items, hard_coded_items)
|
assert_eq!(lisp_items, hard_coded_items)
|
||||||
// }
|
}
|
||||||
// _ => panic!("this should be a lisp"),
|
_ => panic!("this should be a lisp"),
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Planning on removing lisp potentially
|
#[test]
|
||||||
// // #[test]
|
fn test_parsing_lisp_presentation() {
|
||||||
// // fn test_parsing_lisp_presentation() {
|
let lisp = read_to_string("./testypres.lisp").expect("oops");
|
||||||
// // let lisp = read_to_string("./testypres.lisp").expect("oops");
|
let lisp_value = crisp::reader::read(&lisp);
|
||||||
// // let lisp_value = crisp::reader::read(&lisp);
|
let hard_coded_items = vec![
|
||||||
// // let hard_coded_items = vec![
|
service_item_1(),
|
||||||
// // service_item_1(),
|
service_item_2(),
|
||||||
// // service_item_2(),
|
service_item_3(),
|
||||||
// // service_item_3(),
|
];
|
||||||
// // ];
|
match lisp_value {
|
||||||
// // match lisp_value {
|
Value::List(value) => {
|
||||||
// // Value::List(value) => {
|
let mut lisp_items = vec![];
|
||||||
// // let mut lisp_items = vec![];
|
for value in value {
|
||||||
// // for value in value {
|
let mut vec = parse_lisp(value);
|
||||||
// // let mut vec = parse_lisp(value);
|
lisp_items.append(&mut vec);
|
||||||
// // lisp_items.append(&mut vec);
|
}
|
||||||
// // }
|
let item_1 = &lisp_items[0];
|
||||||
// // let item_1 = &lisp_items[0];
|
let item_2 = &lisp_items[1];
|
||||||
// // let item_2 = &lisp_items[1];
|
let item_3 = &lisp_items[2];
|
||||||
// // let item_3 = &lisp_items[2];
|
assert_eq!(item_1, &hard_coded_items[0]);
|
||||||
// // assert_eq!(item_1, &hard_coded_items[0]);
|
assert_eq!(item_2, &hard_coded_items[1]);
|
||||||
// // assert_eq!(item_2, &hard_coded_items[1]);
|
assert_eq!(item_3, &hard_coded_items[2]);
|
||||||
// // assert_eq!(item_3, &hard_coded_items[2]);
|
|
||||||
|
|
||||||
// // assert_eq!(lisp_items, hard_coded_items);
|
assert_eq!(lisp_items, hard_coded_items);
|
||||||
// // }
|
}
|
||||||
// // _ => panic!("this should be a lisp"),
|
_ => panic!("this should be a lisp"),
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// fn service_item_1() -> ServiceItem {
|
fn service_item_1() -> ServiceItem {
|
||||||
// let image = Image {
|
let image = Image {
|
||||||
// title: "This is frodo".to_string(),
|
title: "This is frodo".to_string(),
|
||||||
// path: PathBuf::from("~/pics/frodo.jpg"),
|
path: PathBuf::from("~/pics/frodo.jpg"),
|
||||||
// ..Default::default()
|
..Default::default()
|
||||||
// };
|
};
|
||||||
// let slide = &image.to_slides().unwrap()[0];
|
let slide = &image.to_slides().unwrap()[0];
|
||||||
// let slide = slide
|
let slide = slide
|
||||||
// .clone()
|
.clone()
|
||||||
// .set_text("This is frodo")
|
.set_text("This is frodo")
|
||||||
// .set_font("Quicksand")
|
.set_font("Quicksand")
|
||||||
// .set_font_size(70)
|
.set_font_size(70)
|
||||||
// .set_audio(None);
|
.set_audio(None);
|
||||||
// ServiceItem {
|
ServiceItem {
|
||||||
// title: "This is frodo".to_string(),
|
title: "This is frodo".to_string(),
|
||||||
// kind: ServiceItemKind::Content(slide.clone()),
|
kind: ServiceItemKind::Content(slide.clone()),
|
||||||
// slides: vec![slide],
|
..Default::default()
|
||||||
// ..Default::default()
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// fn service_item_2() -> ServiceItem {
|
fn service_item_2() -> ServiceItem {
|
||||||
// let video = Video::from(PathBuf::from(
|
ServiceItem {
|
||||||
// "~/vids/test/camprules2024.mp4",
|
title: "camprules2024.mp4".to_string(),
|
||||||
// ));
|
kind: ServiceItemKind::Video(Video {
|
||||||
// let slide = &video.to_slides().unwrap()[0];
|
title: "camprules2024.mp4".to_string(),
|
||||||
// ServiceItem {
|
path: PathBuf::from("~/vids/test/camprules2024.mp4"),
|
||||||
// title: "camprules2024.mp4".to_string(),
|
start_time: None,
|
||||||
// kind: ServiceItemKind::Video(Video {
|
end_time: None,
|
||||||
// title: "camprules2024.mp4".to_string(),
|
looping: false,
|
||||||
// path: PathBuf::from("~/vids/test/camprules2024.mp4"),
|
..Default::default()
|
||||||
// start_time: None,
|
}),
|
||||||
// end_time: None,
|
..Default::default()
|
||||||
// looping: false,
|
}
|
||||||
// ..Default::default()
|
}
|
||||||
// }),
|
|
||||||
// slides: vec![slide.clone()],
|
|
||||||
// ..Default::default()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn service_item_3() -> ServiceItem {
|
fn service_item_3() -> ServiceItem {
|
||||||
// ServiceItem {
|
ServiceItem {
|
||||||
// title: "Death Was Arrested".to_string(),
|
title: "Death Was Arrested".to_string(),
|
||||||
// kind: ServiceItemKind::Song(test_song()),
|
kind: ServiceItemKind::Song(test_song()),
|
||||||
// database_id: 7,
|
database_id: 7,
|
||||||
// ..Default::default()
|
..Default::default()
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// fn test_song() -> Song {
|
fn test_song() -> Song {
|
||||||
// Song {
|
Song {
|
||||||
// id: 7,
|
id: 7,
|
||||||
// title: "Death Was Arrested".to_string(),
|
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()),
|
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(
|
author: Some(
|
||||||
// "North Point Worship".to_string(),
|
"North Point Worship".to_string(),
|
||||||
// ),
|
),
|
||||||
// ccli: None,
|
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()),
|
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![
|
verse_order: Some(vec![
|
||||||
// "I1".to_string(),
|
"I1".to_string(),
|
||||||
// "V1".to_string(),
|
"V1".to_string(),
|
||||||
// "V2".to_string(),
|
"V2".to_string(),
|
||||||
// "C1".to_string(),
|
"C1".to_string(),
|
||||||
// "V3".to_string(),
|
"V3".to_string(),
|
||||||
// "C1".to_string(),
|
"C1".to_string(),
|
||||||
// "V4".to_string(),
|
"V4".to_string(),
|
||||||
// "C1".to_string(),
|
"C1".to_string(),
|
||||||
// "B1".to_string(),
|
"B1".to_string(),
|
||||||
// "B1".to_string(),
|
"B1".to_string(),
|
||||||
// "E1".to_string(),
|
"E1".to_string(),
|
||||||
// "E2".to_string(),
|
"E2".to_string(),
|
||||||
// ]),
|
]),
|
||||||
// background: Some(Background::try_from("file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg").unwrap()),
|
background: Some(Background::try_from("file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg").unwrap()),
|
||||||
// text_alignment: Some(TextAlignment::MiddleCenter),
|
text_alignment: Some(TextAlignment::MiddleCenter),
|
||||||
// font: Some("Quicksand Bold".to_string()),
|
font: Some("Quicksand Bold".to_string()),
|
||||||
// font_size: Some(60),
|
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!(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
2219
src/main.rs
|
|
@ -1,6 +1,6 @@
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use cosmic::iced::Length;
|
use iced::Length;
|
||||||
|
|
||||||
struct DoubleSlider<'a, T, Message> {
|
struct DoubleSlider<'a, T, Message> {
|
||||||
range: RangeInclusive<T>,
|
range: RangeInclusive<T>,
|
||||||
|
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
use std::{io, path::PathBuf};
|
|
||||||
|
|
||||||
use crate::core::images::Image;
|
|
||||||
use cosmic::{
|
|
||||||
Apply, Element, Task,
|
|
||||||
dialog::file_chooser::{FileFilter, open::Dialog},
|
|
||||||
iced::{Length, alignment::Vertical},
|
|
||||||
iced_widget::{column, row},
|
|
||||||
theme,
|
|
||||||
widget::{
|
|
||||||
self, Space, button, container, horizontal_space, icon, text,
|
|
||||||
text_input,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use tracing::{debug, error, warn};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ImageEditor {
|
|
||||||
pub image: Option<Image>,
|
|
||||||
title: String,
|
|
||||||
editing: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Action {
|
|
||||||
Task(Task<Message>),
|
|
||||||
UpdateImage(Image),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Message {
|
|
||||||
ChangeImage(Image),
|
|
||||||
Update(Image),
|
|
||||||
ChangeTitle(String),
|
|
||||||
PickImage,
|
|
||||||
Edit(bool),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImageEditor {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
image: None,
|
|
||||||
title: "Death was Arrested".to_string(),
|
|
||||||
editing: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn update(&mut self, message: Message) -> Action {
|
|
||||||
match message {
|
|
||||||
Message::ChangeImage(image) => {
|
|
||||||
self.update_entire_image(&image);
|
|
||||||
}
|
|
||||||
Message::ChangeTitle(title) => {
|
|
||||||
self.title.clone_from(&title);
|
|
||||||
if let Some(image) = &self.image {
|
|
||||||
let mut image = image.clone();
|
|
||||||
image.title = title;
|
|
||||||
return self.update(Message::Update(image));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::Edit(edit) => {
|
|
||||||
debug!(edit);
|
|
||||||
self.editing = edit;
|
|
||||||
}
|
|
||||||
Message::Update(image) => {
|
|
||||||
warn!(?image);
|
|
||||||
self.update_entire_image(&image);
|
|
||||||
return Action::UpdateImage(image);
|
|
||||||
}
|
|
||||||
Message::PickImage => {
|
|
||||||
let image_id = self
|
|
||||||
.image
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.id)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let task = Task::perform(
|
|
||||||
pick_image(),
|
|
||||||
move |image_result| {
|
|
||||||
image_result.map_or(Message::None, |image| {
|
|
||||||
let mut image = Image::from(image);
|
|
||||||
image.id = image_id;
|
|
||||||
Message::Update(image)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Action::Task(task);
|
|
||||||
}
|
|
||||||
Message::None => (),
|
|
||||||
}
|
|
||||||
Action::None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 column = column![
|
|
||||||
self.toolbar(),
|
|
||||||
container.center_x(Length::FillPortion(2))
|
|
||||||
]
|
|
||||||
.spacing(theme::active().cosmic().space_l());
|
|
||||||
column.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toolbar(&self) -> Element<Message> {
|
|
||||||
let title_box = text_input("Title...", &self.title)
|
|
||||||
.on_input(Message::ChangeTitle);
|
|
||||||
|
|
||||||
let image_selector = button::icon(
|
|
||||||
icon::from_name("folder-images-symbolic").scale(2),
|
|
||||||
)
|
|
||||||
.label("Image")
|
|
||||||
.tooltip("Select a image")
|
|
||||||
.on_press(Message::PickImage)
|
|
||||||
.padding(10);
|
|
||||||
|
|
||||||
row![
|
|
||||||
text::body("Title:"),
|
|
||||||
title_box,
|
|
||||||
horizontal_space(),
|
|
||||||
image_selector
|
|
||||||
]
|
|
||||||
.align_y(Vertical::Center)
|
|
||||||
.spacing(10)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn editing(&self) -> bool {
|
|
||||||
self.editing
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_entire_image(&mut self, image: &Image) {
|
|
||||||
self.image = Some(image.clone());
|
|
||||||
self.title.clone_from(&image.title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ImageEditor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pick_image() -> Result<PathBuf, ImageError> {
|
|
||||||
let dialog = Dialog::new().title("Choose a image...");
|
|
||||||
let bg_filter = FileFilter::new("Images")
|
|
||||||
.extension("png")
|
|
||||||
.extension("jpeg")
|
|
||||||
.extension("gif")
|
|
||||||
.extension("heic")
|
|
||||||
.extension("webp")
|
|
||||||
.extension("jpg");
|
|
||||||
dialog
|
|
||||||
.filter(bg_filter)
|
|
||||||
.directory(dirs::home_dir().expect("oops"))
|
|
||||||
.open_file()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(?e);
|
|
||||||
ImageError::DialogClosed
|
|
||||||
})
|
|
||||||
.map(|file| {
|
|
||||||
file.url().to_file_path().expect("Should be a file here")
|
|
||||||
})
|
|
||||||
// rfd::AsyncFileDialog::new()
|
|
||||||
// .set_title("Choose a background...")
|
|
||||||
// .add_filter(
|
|
||||||
// "Images and Images",
|
|
||||||
// &["png", "jpeg", "mp4", "webm", "mkv", "jpg", "mpeg"],
|
|
||||||
// )
|
|
||||||
// .set_directory(dirs::home_dir().unwrap())
|
|
||||||
// .pick_file()
|
|
||||||
// .await
|
|
||||||
// .ok_or(ImageError::BackgroundDialogClosed)
|
|
||||||
// .map(|file| file.path().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ImageError {
|
|
||||||
DialogClosed,
|
|
||||||
IOError(io::ErrorKind),
|
|
||||||
}
|
|
||||||
1383
src/ui/library.rs
|
|
@ -1,16 +1,12 @@
|
||||||
use crate::core::model::LibraryKind;
|
use crate::core::model::LibraryKind;
|
||||||
|
|
||||||
// pub mod double_ended_slider;
|
pub mod double_ended_slider;
|
||||||
pub mod image_editor;
|
|
||||||
pub mod library;
|
pub mod library;
|
||||||
pub mod presentation_editor;
|
|
||||||
pub mod presenter;
|
pub mod presenter;
|
||||||
pub mod service;
|
|
||||||
pub mod slide_editor;
|
pub mod slide_editor;
|
||||||
pub mod song_editor;
|
pub mod song_editor;
|
||||||
pub mod text_svg;
|
pub mod text_svg;
|
||||||
pub mod video;
|
pub mod video;
|
||||||
pub mod video_editor;
|
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
pub enum EditorMode {
|
pub enum EditorMode {
|
||||||
|
|
|
||||||
|
|
@ -1,730 +0,0 @@
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
io,
|
|
||||||
ops::RangeBounds,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::core::presentations::{PresKind, Presentation};
|
|
||||||
use cosmic::{
|
|
||||||
Element, Task,
|
|
||||||
dialog::file_chooser::{FileFilter, open::Dialog},
|
|
||||||
iced::{Background, ContentFit, Length, alignment::Vertical},
|
|
||||||
iced_widget::{column, row},
|
|
||||||
theme,
|
|
||||||
widget::{
|
|
||||||
self, Space, button, container, context_menu,
|
|
||||||
horizontal_space, icon, image::Handle, menu, mouse_area,
|
|
||||||
scrollable, text, text_input,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
|
||||||
use mupdf::{Colorspace, Document, Matrix};
|
|
||||||
use tracing::{debug, error, warn};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PresentationEditor {
|
|
||||||
pub presentation: Option<Presentation>,
|
|
||||||
document: Option<Document>,
|
|
||||||
current_slide: Option<Handle>,
|
|
||||||
slides: Option<Vec<Handle>>,
|
|
||||||
page_count: Option<i32>,
|
|
||||||
current_slide_index: Option<i32>,
|
|
||||||
title: String,
|
|
||||||
editing: bool,
|
|
||||||
hovered_slide: Option<i32>,
|
|
||||||
context_menu_id: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Action {
|
|
||||||
Task(Task<Message>),
|
|
||||||
UpdatePresentation(Presentation),
|
|
||||||
SplitAddPresentation((Presentation, Presentation)),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Message {
|
|
||||||
ChangePresentation(Presentation),
|
|
||||||
Update(Presentation),
|
|
||||||
ChangeTitle(String),
|
|
||||||
PickPresentation,
|
|
||||||
Edit(bool),
|
|
||||||
NextPage,
|
|
||||||
PrevPage,
|
|
||||||
None,
|
|
||||||
ChangePresentationFile(Presentation),
|
|
||||||
AddSlides(Option<Vec<Handle>>),
|
|
||||||
ChangeSlide(usize),
|
|
||||||
HoverSlide(Option<i32>),
|
|
||||||
ContextMenu(usize),
|
|
||||||
SplitBefore,
|
|
||||||
SplitAfter,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum MenuAction {
|
|
||||||
SplitBefore,
|
|
||||||
SplitAfter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl menu::Action for MenuAction {
|
|
||||||
type Message = Message;
|
|
||||||
|
|
||||||
fn message(&self) -> Self::Message {
|
|
||||||
match self {
|
|
||||||
Self::SplitBefore => Message::SplitBefore,
|
|
||||||
Self::SplitAfter => Message::SplitAfter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PresentationEditor {
|
|
||||||
#[must_use]
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
presentation: None,
|
|
||||||
document: None,
|
|
||||||
title: String::new(),
|
|
||||||
editing: false,
|
|
||||||
current_slide: None,
|
|
||||||
current_slide_index: None,
|
|
||||||
page_count: None,
|
|
||||||
slides: None,
|
|
||||||
hovered_slide: None,
|
|
||||||
context_menu_id: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn update(&mut self, message: Message) -> Action {
|
|
||||||
match message {
|
|
||||||
Message::ChangePresentation(presentation) => {
|
|
||||||
self.update_entire_presentation(&presentation);
|
|
||||||
if let Some(presentation) = &self.presentation {
|
|
||||||
let task;
|
|
||||||
let path = presentation.path.clone();
|
|
||||||
if let PresKind::Pdf {
|
|
||||||
starting_index,
|
|
||||||
ending_index,
|
|
||||||
} = presentation.kind.clone()
|
|
||||||
{
|
|
||||||
let range = starting_index..=ending_index;
|
|
||||||
task = Task::perform(
|
|
||||||
async move { get_pages(range, path) },
|
|
||||||
Message::AddSlides,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
task = Task::perform(
|
|
||||||
async move { get_pages(.., path) },
|
|
||||||
Message::AddSlides,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Action::Task(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::ChangeTitle(title) => {
|
|
||||||
self.title.clone_from(&title);
|
|
||||||
if let Some(presentation) = &self.presentation {
|
|
||||||
let mut presentation = presentation.clone();
|
|
||||||
presentation.title = title;
|
|
||||||
return self
|
|
||||||
.update(Message::Update(presentation));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::Edit(edit) => {
|
|
||||||
debug!(edit);
|
|
||||||
self.editing = edit;
|
|
||||||
}
|
|
||||||
Message::Update(presentation) => {
|
|
||||||
warn!(?presentation, "about to update");
|
|
||||||
return Action::UpdatePresentation(presentation);
|
|
||||||
}
|
|
||||||
Message::PickPresentation => {
|
|
||||||
let presentation_id = self
|
|
||||||
.presentation
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.id)
|
|
||||||
.unwrap_or_default();
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Action::Task(task);
|
|
||||||
}
|
|
||||||
Message::ChangePresentationFile(presentation) => {
|
|
||||||
self.update_entire_presentation(&presentation);
|
|
||||||
if let Some(presentation) = &self.presentation {
|
|
||||||
let mut task;
|
|
||||||
let path = presentation.path.clone();
|
|
||||||
if let PresKind::Pdf {
|
|
||||||
starting_index,
|
|
||||||
ending_index,
|
|
||||||
} = presentation.kind.clone()
|
|
||||||
{
|
|
||||||
let range = starting_index..=ending_index;
|
|
||||||
task = Task::perform(
|
|
||||||
async move { get_pages(range, path) },
|
|
||||||
Message::AddSlides,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
task = Task::perform(
|
|
||||||
async move { get_pages(.., path) },
|
|
||||||
Message::AddSlides,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
task = task.chain(Task::done(Message::Update(
|
|
||||||
presentation.clone(),
|
|
||||||
)));
|
|
||||||
return Action::Task(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::AddSlides(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 PresKind::Pdf { ending_index, .. } =
|
|
||||||
presentation.kind
|
|
||||||
{
|
|
||||||
ending_index
|
|
||||||
} else {
|
|
||||||
self.page_count.unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
if next_index > last_index {
|
|
||||||
return Action::None;
|
|
||||||
}
|
|
||||||
self.current_slide =
|
|
||||||
self.document.as_ref().and_then(|doc| {
|
|
||||||
let page = doc.load_page(next_index).ok()?;
|
|
||||||
let matrix = Matrix::IDENTITY;
|
|
||||||
let colorspace = Colorspace::device_rgb();
|
|
||||||
let Ok(pixmap) = page
|
|
||||||
.to_pixmap(
|
|
||||||
&matrix,
|
|
||||||
&colorspace,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.into_diagnostic()
|
|
||||||
else {
|
|
||||||
error!(
|
|
||||||
"Can't turn this page into pixmap"
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
debug!(?pixmap);
|
|
||||||
Some(Handle::from_rgba(
|
|
||||||
pixmap.width(),
|
|
||||||
pixmap.height(),
|
|
||||||
pixmap.samples().to_vec(),
|
|
||||||
))
|
|
||||||
});
|
|
||||||
self.current_slide_index = Some(next_index);
|
|
||||||
}
|
|
||||||
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 PresKind::Pdf { starting_index, .. } =
|
|
||||||
presentation.kind
|
|
||||||
{
|
|
||||||
starting_index
|
|
||||||
} else {
|
|
||||||
self.page_count.unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
if previous_index < first_index {
|
|
||||||
return Action::None;
|
|
||||||
}
|
|
||||||
self.current_slide =
|
|
||||||
self.document.as_ref().and_then(|doc| {
|
|
||||||
let page =
|
|
||||||
doc.load_page(previous_index).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(previous_index);
|
|
||||||
}
|
|
||||||
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 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 = i32::try_from(index).ok();
|
|
||||||
}
|
|
||||||
Message::HoverSlide(slide) => {
|
|
||||||
self.hovered_slide = slide;
|
|
||||||
}
|
|
||||||
Message::ContextMenu(index) => {
|
|
||||||
self.context_menu_id = i32::try_from(index).ok();
|
|
||||||
}
|
|
||||||
Message::SplitBefore => {
|
|
||||||
if let Ok((first, second)) = self.split_before() {
|
|
||||||
debug!(?first, ?second);
|
|
||||||
self.update_entire_presentation(&first);
|
|
||||||
return Action::SplitAddPresentation((
|
|
||||||
first, second,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::SplitAfter => {
|
|
||||||
if let Ok((first, second)) = self.split_after() {
|
|
||||||
debug!(?first, ?second);
|
|
||||||
self.update_entire_presentation(&first);
|
|
||||||
return Action::SplitAddPresentation((
|
|
||||||
first, second,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Action::None
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let pages_column = container(
|
|
||||||
self.context_menu(
|
|
||||||
scrollable(
|
|
||||||
column(pdf_pages)
|
|
||||||
.spacing(theme::active().cosmic().space_xs())
|
|
||||||
.padding(theme::spacing().space_xs),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.class(theme::Container::Card);
|
|
||||||
let main_row = row![
|
|
||||||
pages_column,
|
|
||||||
container(presentation).center(Length::FillPortion(2))
|
|
||||||
]
|
|
||||||
.spacing(theme::spacing().space_xxl);
|
|
||||||
let control_buttons = row![
|
|
||||||
button::standard("Previous Page")
|
|
||||||
.on_press(Message::PrevPage),
|
|
||||||
horizontal_space(),
|
|
||||||
button::standard("Next Page").on_press(Message::NextPage),
|
|
||||||
];
|
|
||||||
let column =
|
|
||||||
column![self.toolbar(), main_row, control_buttons]
|
|
||||||
.spacing(theme::active().cosmic().space_l());
|
|
||||||
column.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 presentation_selector = button::icon(
|
|
||||||
icon::from_name("folder-presentations-symbolic").scale(2),
|
|
||||||
)
|
|
||||||
.label("Change Presentation")
|
|
||||||
.tooltip("Select a presentation")
|
|
||||||
.on_press(Message::PickPresentation)
|
|
||||||
.padding(10);
|
|
||||||
|
|
||||||
row![
|
|
||||||
text::body("Title:"),
|
|
||||||
title_box,
|
|
||||||
horizontal_space(),
|
|
||||||
presentation_selector
|
|
||||||
]
|
|
||||||
.align_y(Vertical::Center)
|
|
||||||
.spacing(10)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn editing(&self) -> bool {
|
|
||||||
self.editing
|
|
||||||
}
|
|
||||||
|
|
||||||
fn context_menu<'b>(
|
|
||||||
&self,
|
|
||||||
items: Element<'b, Message>,
|
|
||||||
) -> Element<'b, Message> {
|
|
||||||
if self.context_menu_id.is_some() {
|
|
||||||
let before_icon =
|
|
||||||
icon::from_path("./res/split-above.svg".into())
|
|
||||||
.symbolic(true);
|
|
||||||
let after_icon =
|
|
||||||
icon::from_path("./res/split-below.svg".into())
|
|
||||||
.symbolic(true);
|
|
||||||
let menu_items = vec![
|
|
||||||
menu::Item::Button(
|
|
||||||
"Spit Before",
|
|
||||||
Some(before_icon),
|
|
||||||
MenuAction::SplitBefore,
|
|
||||||
),
|
|
||||||
menu::Item::Button(
|
|
||||||
"Split After",
|
|
||||||
Some(after_icon),
|
|
||||||
MenuAction::SplitAfter,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
let context_menu = context_menu(
|
|
||||||
items,
|
|
||||||
self.context_menu_id.map_or_else(
|
|
||||||
|| None,
|
|
||||||
|_| {
|
|
||||||
Some(menu::items(&HashMap::new(), menu_items))
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Element::from(context_menu)
|
|
||||||
} else {
|
|
||||||
items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_entire_presentation(
|
|
||||||
&mut self,
|
|
||||||
presentation: &Presentation,
|
|
||||||
) {
|
|
||||||
self.presentation = Some(presentation.clone());
|
|
||||||
self.title.clone_from(&presentation.title);
|
|
||||||
self.document =
|
|
||||||
Document::open(&presentation.path.as_path()).ok();
|
|
||||||
self.page_count = self
|
|
||||||
.document
|
|
||||||
.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()?;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_before(&self) -> Result<(Presentation, Presentation)> {
|
|
||||||
if let Some(index) = self.context_menu_id {
|
|
||||||
let Some(current_presentation) =
|
|
||||||
self.presentation.as_ref()
|
|
||||||
else {
|
|
||||||
return Err(miette!(
|
|
||||||
"There is no current presentation"
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let first_presentation = Presentation {
|
|
||||||
id: current_presentation.id,
|
|
||||||
title: current_presentation.title.clone(),
|
|
||||||
path: current_presentation.path.clone(),
|
|
||||||
kind: match current_presentation.kind {
|
|
||||||
PresKind::Pdf { .. } => PresKind::Pdf {
|
|
||||||
starting_index: 0,
|
|
||||||
ending_index: index - 1,
|
|
||||||
},
|
|
||||||
_ => current_presentation.kind.clone(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let second_presentation = Presentation {
|
|
||||||
id: 0,
|
|
||||||
title: format!(
|
|
||||||
"{} (2)",
|
|
||||||
current_presentation.title.clone()
|
|
||||||
),
|
|
||||||
path: current_presentation.path.clone(),
|
|
||||||
kind: match current_presentation.kind {
|
|
||||||
PresKind::Pdf { ending_index, .. } => {
|
|
||||||
PresKind::Pdf {
|
|
||||||
starting_index: index,
|
|
||||||
ending_index,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => current_presentation.kind.clone(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok((first_presentation, second_presentation))
|
|
||||||
} else {
|
|
||||||
error!("split before no index");
|
|
||||||
Err(miette!(
|
|
||||||
"No current index from context menu, has there been a right click on a presentation page"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn split_after(&self) -> Result<(Presentation, Presentation)> {
|
|
||||||
if let Some(index) = self.context_menu_id {
|
|
||||||
let Some(current_presentation) =
|
|
||||||
self.presentation.as_ref()
|
|
||||||
else {
|
|
||||||
return Err(miette!(
|
|
||||||
"There is no current presentation"
|
|
||||||
));
|
|
||||||
};
|
|
||||||
let first_presentation = Presentation {
|
|
||||||
id: current_presentation.id,
|
|
||||||
title: current_presentation.title.clone(),
|
|
||||||
path: current_presentation.path.clone(),
|
|
||||||
kind: match current_presentation.kind {
|
|
||||||
PresKind::Pdf { .. } => PresKind::Pdf {
|
|
||||||
starting_index: 0,
|
|
||||||
ending_index: index,
|
|
||||||
},
|
|
||||||
_ => current_presentation.kind.clone(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let second_presentation = Presentation {
|
|
||||||
id: 0,
|
|
||||||
title: format!(
|
|
||||||
"{} (2)",
|
|
||||||
current_presentation.title.clone()
|
|
||||||
),
|
|
||||||
path: current_presentation.path.clone(),
|
|
||||||
kind: match current_presentation.kind {
|
|
||||||
PresKind::Pdf { ending_index, .. } => {
|
|
||||||
PresKind::Pdf {
|
|
||||||
starting_index: index + 1,
|
|
||||||
ending_index,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => current_presentation.kind.clone(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok((first_presentation, second_presentation))
|
|
||||||
} else {
|
|
||||||
error!("split before no index");
|
|
||||||
Err(miette!(
|
|
||||||
"No current index from context menu, has there been a right click on a presentation page"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PresentationEditor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_pages(
|
|
||||||
range: impl RangeBounds<i32>,
|
|
||||||
presentation_path: impl AsRef<Path>,
|
|
||||||
) -> Option<Vec<Handle>> {
|
|
||||||
let document = Document::open(presentation_path.as_ref()).ok()?;
|
|
||||||
let pages = document.pages().ok()?;
|
|
||||||
Some(
|
|
||||||
pages
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(index, page)| {
|
|
||||||
if !range.contains(&i32::try_from(index).expect(
|
|
||||||
"looking for a pdf index that is way too large",
|
|
||||||
)) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let page = page.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(),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pick_presentation() -> Result<PathBuf, PresentationError> {
|
|
||||||
let dialog = Dialog::new().title("Choose a presentation...");
|
|
||||||
let bg_filter = FileFilter::new("Presentations")
|
|
||||||
.extension("pdf")
|
|
||||||
.extension("html");
|
|
||||||
dialog
|
|
||||||
.filter(bg_filter)
|
|
||||||
.directory(dirs::home_dir().expect("oops"))
|
|
||||||
.open_file()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(?e);
|
|
||||||
PresentationError::DialogClosed
|
|
||||||
})
|
|
||||||
.map(|file| {
|
|
||||||
file.url().to_file_path().expect("Should be a file here")
|
|
||||||
})
|
|
||||||
// rfd::AsyncFileDialog::new()
|
|
||||||
// .set_title("Choose a background...")
|
|
||||||
// .add_filter(
|
|
||||||
// "Presentations and Presentations",
|
|
||||||
// &["png", "jpeg", "mp4", "webm", "mkv", "jpg", "mpeg"],
|
|
||||||
// )
|
|
||||||
// .set_directory(dirs::home_dir().unwrap())
|
|
||||||
// .pick_file()
|
|
||||||
// .await
|
|
||||||
// .ok_or(PresentationError::BackgroundDialogClosed)
|
|
||||||
// .map(|file| file.path().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum PresentationError {
|
|
||||||
DialogClosed,
|
|
||||||
IOError(io::ErrorKind),
|
|
||||||
}
|
|
||||||
|
|
@ -1,354 +0,0 @@
|
||||||
use cosmic::iced::Size;
|
|
||||||
|
|
||||||
use cosmic::iced_core::widget::tree;
|
|
||||||
use cosmic::{
|
|
||||||
Element,
|
|
||||||
iced::{
|
|
||||||
Event, Length, Point, Rectangle, Vector,
|
|
||||||
clipboard::dnd::{DndEvent, SourceEvent},
|
|
||||||
event, mouse,
|
|
||||||
},
|
|
||||||
iced_core::{
|
|
||||||
self, Clipboard, Shell, layout, renderer, widget::Tree,
|
|
||||||
},
|
|
||||||
widget::Widget,
|
|
||||||
};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::core::service_items::ServiceItem;
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn service<Message: Clone + 'static>(
|
|
||||||
service: &Vec<ServiceItem>,
|
|
||||||
) -> Service<'_, Message> {
|
|
||||||
Service::new(service)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Service<'a, Message> {
|
|
||||||
items: &'a Vec<ServiceItem>,
|
|
||||||
on_start: Option<Message>,
|
|
||||||
on_cancelled: Option<Message>,
|
|
||||||
on_finish: Option<Message>,
|
|
||||||
drag_threshold: f32,
|
|
||||||
width: Length,
|
|
||||||
height: Length,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message: Clone + 'static> Service<'a, Message> {
|
|
||||||
#[must_use]
|
|
||||||
pub const fn new(service_items: &'a Vec<ServiceItem>) -> Self {
|
|
||||||
Self {
|
|
||||||
items: service_items,
|
|
||||||
drag_threshold: 8.0,
|
|
||||||
on_start: None,
|
|
||||||
on_cancelled: None,
|
|
||||||
on_finish: None,
|
|
||||||
width: Length::Fill,
|
|
||||||
height: Length::Fill,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn drag_threshold(mut self, threshold: f32) -> Self {
|
|
||||||
self.drag_threshold = threshold;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn start_dnd(
|
|
||||||
// &self,
|
|
||||||
// clipboard: &mut dyn Clipboard,
|
|
||||||
// bounds: Rectangle,
|
|
||||||
// offset: Vector,
|
|
||||||
// ) {
|
|
||||||
// let Some(content) = self.drag_content.as_ref().map(|f| f())
|
|
||||||
// else {
|
|
||||||
// return;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// iced_core::clipboard::start_dnd(
|
|
||||||
// clipboard,
|
|
||||||
// false,
|
|
||||||
// if let Some(window) = self.window.as_ref() {
|
|
||||||
// Some(iced_core::clipboard::DndSource::Surface(
|
|
||||||
// *window,
|
|
||||||
// ))
|
|
||||||
// } else {
|
|
||||||
// Some(iced_core::clipboard::DndSource::Widget(
|
|
||||||
// self.id.clone(),
|
|
||||||
// ))
|
|
||||||
// },
|
|
||||||
// self.drag_icon.as_ref().map(|f| {
|
|
||||||
// let (icon, state, offset) = f(offset);
|
|
||||||
// iced_core::clipboard::IconSurface::new(
|
|
||||||
// container(icon)
|
|
||||||
// .width(Length::Fixed(bounds.width))
|
|
||||||
// .height(Length::Fixed(bounds.height))
|
|
||||||
// .into(),
|
|
||||||
// state,
|
|
||||||
// offset,
|
|
||||||
// )
|
|
||||||
// }),
|
|
||||||
// Box::new(content),
|
|
||||||
// DndAction::Move,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[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>,
|
|
||||||
) -> Self {
|
|
||||||
self.on_cancelled = on_cancelled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn on_finish(mut self, on_finish: Option<Message>) -> Self {
|
|
||||||
self.on_finish = on_finish;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Message: Clone + 'static>
|
|
||||||
Widget<Message, cosmic::Theme, cosmic::Renderer>
|
|
||||||
for Service<'_, Message>
|
|
||||||
{
|
|
||||||
fn size(&self) -> iced_core::Size<Length> {
|
|
||||||
Size {
|
|
||||||
width: Length::Fill,
|
|
||||||
height: Length::Fill,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&self,
|
|
||||||
_tree: &mut Tree,
|
|
||||||
_renderer: &cosmic::Renderer,
|
|
||||||
limits: &layout::Limits,
|
|
||||||
) -> layout::Node {
|
|
||||||
layout::atomic(limits, self.width, self.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> iced_core::widget::tree::State {
|
|
||||||
tree::State::new(State::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
// fn operate(
|
|
||||||
// &self,
|
|
||||||
// tree: &mut Tree,
|
|
||||||
// layout: layout::Layout<'_>,
|
|
||||||
// renderer: &cosmic::Renderer,
|
|
||||||
// operation: &mut dyn iced_core::widget::Operation<()>,
|
|
||||||
// ) {
|
|
||||||
// operation.custom(
|
|
||||||
// (&mut tree.state) as &mut dyn Any,
|
|
||||||
// Some(&self.id),
|
|
||||||
// );
|
|
||||||
// operation.container(
|
|
||||||
// Some(&self.id),
|
|
||||||
// layout.bounds(),
|
|
||||||
// &mut |operation| {
|
|
||||||
// self.container.as_widget().operate(
|
|
||||||
// &mut tree.children[0],
|
|
||||||
// layout,
|
|
||||||
// renderer,
|
|
||||||
// operation,
|
|
||||||
// )
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn on_event(
|
|
||||||
&mut self,
|
|
||||||
tree: &mut Tree,
|
|
||||||
event: Event,
|
|
||||||
layout: layout::Layout<'_>,
|
|
||||||
cursor: mouse::Cursor,
|
|
||||||
_renderer: &cosmic::Renderer,
|
|
||||||
_clipboard: &mut dyn Clipboard,
|
|
||||||
shell: &mut Shell<'_, Message>,
|
|
||||||
_viewport: &Rectangle,
|
|
||||||
) -> event::Status {
|
|
||||||
let state = tree.state.downcast_mut::<State>();
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Event::Mouse(mouse_event) => match mouse_event {
|
|
||||||
mouse::Event::ButtonPressed(mouse::Button::Left) => {
|
|
||||||
if let Some(position) = cursor.position() {
|
|
||||||
if !state.hovered {
|
|
||||||
return event::Status::Ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.left_pressed_position = Some(position);
|
|
||||||
return event::Status::Captured;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mouse::Event::ButtonReleased(mouse::Button::Left)
|
|
||||||
if state.left_pressed_position.is_some() =>
|
|
||||||
{
|
|
||||||
state.left_pressed_position = None;
|
|
||||||
return event::Status::Captured;
|
|
||||||
}
|
|
||||||
mouse::Event::CursorMoved { .. } => {
|
|
||||||
if let Some(position) = cursor.position() {
|
|
||||||
if state.hovered {
|
|
||||||
// We ignore motion if we do not possess drag content by now.
|
|
||||||
if let Some(left_pressed_position) =
|
|
||||||
state.left_pressed_position
|
|
||||||
&& position
|
|
||||||
.distance(left_pressed_position)
|
|
||||||
> self.drag_threshold
|
|
||||||
{
|
|
||||||
if let Some(on_start) =
|
|
||||||
self.on_start.as_ref()
|
|
||||||
{
|
|
||||||
shell.publish(on_start.clone());
|
|
||||||
}
|
|
||||||
let _offset = Vector::new(
|
|
||||||
left_pressed_position.x
|
|
||||||
- layout.bounds().x,
|
|
||||||
left_pressed_position.y
|
|
||||||
- layout.bounds().y,
|
|
||||||
);
|
|
||||||
state.is_dragging = true;
|
|
||||||
state.left_pressed_position = None;
|
|
||||||
}
|
|
||||||
if !cursor.is_over(layout.bounds()) {
|
|
||||||
state.hovered = false;
|
|
||||||
|
|
||||||
return event::Status::Ignored;
|
|
||||||
}
|
|
||||||
} else if cursor.is_over(layout.bounds()) {
|
|
||||||
state.hovered = true;
|
|
||||||
}
|
|
||||||
return event::Status::Captured;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return event::Status::Ignored,
|
|
||||||
},
|
|
||||||
Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => {
|
|
||||||
debug!("canceled");
|
|
||||||
if state.is_dragging {
|
|
||||||
if let Some(m) = self.on_cancelled.as_ref() {
|
|
||||||
shell.publish(m.clone());
|
|
||||||
}
|
|
||||||
state.is_dragging = false;
|
|
||||||
return event::Status::Captured;
|
|
||||||
}
|
|
||||||
return event::Status::Ignored;
|
|
||||||
}
|
|
||||||
Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => {
|
|
||||||
debug!("dropped");
|
|
||||||
if state.is_dragging {
|
|
||||||
if let Some(m) = self.on_finish.as_ref() {
|
|
||||||
shell.publish(m.clone());
|
|
||||||
}
|
|
||||||
state.is_dragging = false;
|
|
||||||
return event::Status::Captured;
|
|
||||||
}
|
|
||||||
return event::Status::Ignored;
|
|
||||||
}
|
|
||||||
Event::Dnd(event) => debug!(?event),
|
|
||||||
_ => return event::Status::Ignored,
|
|
||||||
}
|
|
||||||
event::Status::Ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
// fn mouse_interaction(
|
|
||||||
// &self,
|
|
||||||
// tree: &Tree,
|
|
||||||
// layout: layout::Layout<'_>,
|
|
||||||
// cursor_position: mouse::Cursor,
|
|
||||||
// viewport: &Rectangle,
|
|
||||||
// renderer: &cosmic::Renderer,
|
|
||||||
// ) -> mouse::Interaction {
|
|
||||||
// let state = tree.state.downcast_ref::<State>();
|
|
||||||
// if state.is_dragging {
|
|
||||||
// return mouse::Interaction::Grabbing;
|
|
||||||
// }
|
|
||||||
// self.container.as_widget().mouse_interaction(
|
|
||||||
// &tree.children[0],
|
|
||||||
// layout,
|
|
||||||
// cursor_position,
|
|
||||||
// viewport,
|
|
||||||
// renderer,
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
_tree: &Tree,
|
|
||||||
_renderer: &mut cosmic::Renderer,
|
|
||||||
_theme: &cosmic::Theme,
|
|
||||||
_renderer_style: &renderer::Style,
|
|
||||||
_layout: layout::Layout<'_>,
|
|
||||||
_cursor_position: mouse::Cursor,
|
|
||||||
_viewport: &Rectangle,
|
|
||||||
) {
|
|
||||||
// let state = tree.state.downcast_mut::<State>();
|
|
||||||
for _item in self.items {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fn overlay<'b>(
|
|
||||||
// &'b mut self,
|
|
||||||
// tree: &'b mut Tree,
|
|
||||||
// layout: layout::Layout<'_>,
|
|
||||||
// renderer: &cosmic::Renderer,
|
|
||||||
// translation: Vector,
|
|
||||||
// ) -> Option<
|
|
||||||
// overlay::Element<
|
|
||||||
// 'b,
|
|
||||||
// Message,
|
|
||||||
// cosmic::Theme,
|
|
||||||
// cosmic::Renderer,
|
|
||||||
// >,
|
|
||||||
// > {
|
|
||||||
// self.container.as_widget_mut().overlay(
|
|
||||||
// &mut tree.children[0],
|
|
||||||
// layout,
|
|
||||||
// renderer,
|
|
||||||
// translation,
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(feature = "a11y")]
|
|
||||||
// /// get the a11y nodes for the widget
|
|
||||||
// fn a11y_nodes(
|
|
||||||
// &self,
|
|
||||||
// layout: iced_core::Layout<'_>,
|
|
||||||
// state: &Tree,
|
|
||||||
// p: mouse::Cursor,
|
|
||||||
// ) -> iced_accessibility::A11yTree {
|
|
||||||
// let c_state = &state.children[0];
|
|
||||||
// self.container.as_widget().a11y_nodes(layout, c_state, p)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message: Clone + 'static> From<Service<'a, Message>>
|
|
||||||
for Element<'a, Message>
|
|
||||||
{
|
|
||||||
fn from(e: Service<'a, Message>) -> Self {
|
|
||||||
Element::new(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Local state of the [`MouseListener`].
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct State {
|
|
||||||
hovered: bool,
|
|
||||||
left_pressed_position: Option<Point>,
|
|
||||||
is_dragging: bool,
|
|
||||||
_cached_bounds: Rectangle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
use std::{io, path::PathBuf};
|
use std::{io, path::PathBuf};
|
||||||
|
|
||||||
use cosmic::{
|
use iced::{
|
||||||
Renderer,
|
|
||||||
iced::{Color, Font, Length, Size},
|
|
||||||
widget::{
|
widget::{
|
||||||
self,
|
self,
|
||||||
canvas::{self, Program, Stroke},
|
canvas::{self, Program, Stroke},
|
||||||
container,
|
container, Canvas,
|
||||||
},
|
},
|
||||||
|
Color, Font, Length, Renderer, Size,
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct State {
|
struct State {
|
||||||
_cache: canvas::Cache,
|
cache: canvas::Cache,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct SlideEditor {
|
pub struct SlideEditor {
|
||||||
_state: State,
|
state: State,
|
||||||
_font: Font,
|
font: Font,
|
||||||
program: EditorProgram,
|
program: EditorProgram,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,16 +34,11 @@ pub enum Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Text {
|
pub struct Text {
|
||||||
_text: String,
|
text: String,
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Image {
|
|
||||||
_source: PathBuf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum SlideWidget {
|
pub enum SlideWidget {
|
||||||
Text(Text),
|
Text(Text),
|
||||||
Image(Image),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -55,14 +49,14 @@ pub enum SlideError {
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct EditorProgram {
|
struct EditorProgram {
|
||||||
_mouse_button_pressed: Option<cosmic::iced::mouse::Button>,
|
mouse_button_pressed: Option<iced::mouse::Button>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlideEditor {
|
impl SlideEditor {
|
||||||
pub fn view(
|
pub fn view<'a>(
|
||||||
&self,
|
&'a self,
|
||||||
_font: Font,
|
font: Font,
|
||||||
) -> cosmic::Element<'_, SlideWidget> {
|
) -> iced::Element<'a, SlideWidget> {
|
||||||
container(
|
container(
|
||||||
widget::canvas(&self.program)
|
widget::canvas(&self.program)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|
@ -72,21 +66,20 @@ impl SlideEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure to use the `cosmic::Theme and cosmic::Renderer` here
|
/// Ensure to use the `iced::Theme and iced::Renderer` here
|
||||||
/// or else it will not compile
|
/// or else it will not compile
|
||||||
#[allow(clippy::extra_unused_lifetimes)]
|
impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
|
||||||
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
|
||||||
for EditorProgram
|
for EditorProgram
|
||||||
{
|
{
|
||||||
type State = ();
|
type State = ();
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
_state: &Self::State,
|
state: &Self::State,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
_theme: &cosmic::Theme,
|
theme: &iced::Theme,
|
||||||
bounds: cosmic::iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
_cursor: cosmic::iced_core::mouse::Cursor,
|
cursor: iced::mouse::Cursor,
|
||||||
) -> Vec<canvas::Geometry<Renderer>> {
|
) -> Vec<canvas::Geometry<Renderer>> {
|
||||||
// We prepare a new `Frame`
|
// We prepare a new `Frame`
|
||||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||||
|
|
@ -95,7 +88,7 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
// We create a `Path` representing a simple circle
|
// We create a `Path` representing a simple circle
|
||||||
let circle = canvas::Path::circle(frame.center(), 50.0);
|
let circle = canvas::Path::circle(frame.center(), 50.0);
|
||||||
let border = canvas::Path::rectangle(
|
let border = canvas::Path::rectangle(
|
||||||
cosmic::iced::Point { x: 10.0, y: 10.0 },
|
iced::Point { x: 10.0, y: 10.0 },
|
||||||
Size::new(frame_rect.width, frame_rect.height),
|
Size::new(frame_rect.width, frame_rect.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -121,21 +114,19 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
fn update(
|
fn update(
|
||||||
&self,
|
&self,
|
||||||
_state: &mut Self::State,
|
_state: &mut Self::State,
|
||||||
event: canvas::Event,
|
event: &iced::Event,
|
||||||
bounds: cosmic::iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
_cursor: cosmic::iced_core::mouse::Cursor,
|
_cursor: iced::mouse::Cursor,
|
||||||
) -> (canvas::event::Status, Option<SlideWidget>) {
|
) -> std::option::Option<iced::widget::Action<SlideWidget>> {
|
||||||
match event {
|
match event {
|
||||||
canvas::Event::Mouse(event) => match event {
|
iced::Event::Mouse(event) => match event {
|
||||||
cosmic::iced::mouse::Event::CursorEntered => {
|
iced::mouse::Event::CursorEntered => {
|
||||||
debug!("cursor entered");
|
debug!("cursor entered")
|
||||||
}
|
}
|
||||||
cosmic::iced::mouse::Event::CursorLeft => {
|
iced::mouse::Event::CursorLeft => {
|
||||||
debug!("cursor left");
|
debug!("cursor left")
|
||||||
}
|
}
|
||||||
cosmic::iced::mouse::Event::CursorMoved {
|
iced::mouse::Event::CursorMoved { position } => {
|
||||||
position,
|
|
||||||
} => {
|
|
||||||
if bounds.x < position.x
|
if bounds.x < position.x
|
||||||
&& bounds.y < position.y
|
&& bounds.y < position.y
|
||||||
&& (bounds.width + bounds.x) > position.x
|
&& (bounds.width + bounds.x) > position.x
|
||||||
|
|
@ -144,29 +135,34 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
debug!(?position, "cursor moved");
|
debug!(?position, "cursor moved");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cosmic::iced::mouse::Event::ButtonPressed(button) => {
|
iced::mouse::Event::ButtonPressed(button) => {
|
||||||
// self.mouse_button_pressed = Some(button);
|
// self.mouse_button_pressed = Some(button);
|
||||||
debug!(?button, "mouse button pressed");
|
debug!(?button, "mouse button pressed")
|
||||||
|
}
|
||||||
|
iced::mouse::Event::ButtonReleased(button) => {
|
||||||
|
debug!(?button, "mouse button released")
|
||||||
|
}
|
||||||
|
iced::mouse::Event::WheelScrolled { delta } => {
|
||||||
|
debug!(?delta, "scroll wheel")
|
||||||
}
|
}
|
||||||
cosmic::iced::mouse::Event::ButtonReleased(
|
|
||||||
button,
|
|
||||||
) => debug!(?button, "mouse button released"),
|
|
||||||
cosmic::iced::mouse::Event::WheelScrolled {
|
|
||||||
delta,
|
|
||||||
} => debug!(?delta, "scroll wheel"),
|
|
||||||
},
|
},
|
||||||
canvas::Event::Touch(_event) => debug!("test"),
|
iced::Event::Touch(event) => debug!("test"),
|
||||||
canvas::Event::Keyboard(_event) => debug!("test"),
|
iced::Event::Keyboard(event) => debug!("test"),
|
||||||
|
iced::Event::Keyboard(event) => todo!(),
|
||||||
|
iced::Event::Mouse(event) => todo!(),
|
||||||
|
iced::Event::Window(event) => todo!(),
|
||||||
|
iced::Event::Touch(event) => todo!(),
|
||||||
|
iced::Event::InputMethod(event) => todo!(),
|
||||||
}
|
}
|
||||||
(canvas::event::Status::Ignored, None)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
_state: &Self::State,
|
_state: &Self::State,
|
||||||
_bounds: cosmic::iced::Rectangle,
|
_bounds: iced::Rectangle,
|
||||||
_cursor: cosmic::iced_core::mouse::Cursor,
|
_cursor: iced::mouse::Cursor,
|
||||||
) -> cosmic::iced_core::mouse::Interaction {
|
) -> iced::mouse::Interaction {
|
||||||
cosmic::iced_core::mouse::Interaction::default()
|
iced::mouse::Interaction::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,19 @@
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{Display, Write},
|
fmt::Display,
|
||||||
fs,
|
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
path::PathBuf,
|
|
||||||
sync::Arc,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use cosmic::{
|
use colors_transform::Rgb;
|
||||||
cosmic_theme::palette::{IntoColor, Srgb, rgb::Rgba},
|
use iced::{
|
||||||
iced::{
|
|
||||||
ContentFit, Length, Size,
|
|
||||||
font::{Style, Weight},
|
font::{Style, Weight},
|
||||||
},
|
widget::{container, svg::Handle, Svg},
|
||||||
prelude::*,
|
Element, Length, Size,
|
||||||
widget::{Image, Space, image::Handle},
|
|
||||||
};
|
};
|
||||||
use derive_more::Debug;
|
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
|
||||||
use rapidhash::v3::rapidhash_v3;
|
|
||||||
use resvg::{
|
|
||||||
tiny_skia::{self, Pixmap},
|
|
||||||
usvg::{Tree, fontdb},
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{TextAlignment, core::slide::Slide};
|
use crate::TextAlignment;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct TextSvg {
|
pub struct TextSvg {
|
||||||
text: String,
|
text: String,
|
||||||
font: Font,
|
font: Font,
|
||||||
|
|
@ -35,25 +21,7 @@ pub struct TextSvg {
|
||||||
stroke: Option<Stroke>,
|
stroke: Option<Stroke>,
|
||||||
fill: Color,
|
fill: Color,
|
||||||
alignment: TextAlignment,
|
alignment: TextAlignment,
|
||||||
pub path: Option<PathBuf>,
|
handle: Option<Handle>,
|
||||||
#[serde(skip)]
|
|
||||||
pub handle: Option<Handle>,
|
|
||||||
#[serde(skip)]
|
|
||||||
#[debug(skip)]
|
|
||||||
fontdb: Arc<resvg::usvg::fontdb::Database>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for TextSvg {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.text == other.text
|
|
||||||
&& self.font == other.font
|
|
||||||
&& self.shadow == other.shadow
|
|
||||||
&& self.stroke == other.stroke
|
|
||||||
&& self.fill == other.fill
|
|
||||||
&& self.alignment == other.alignment
|
|
||||||
&& self.handle == other.handle
|
|
||||||
&& self.path == other.path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for TextSvg {
|
impl Hash for TextSvg {
|
||||||
|
|
@ -64,13 +32,10 @@ impl Hash for TextSvg {
|
||||||
self.stroke.hash(state);
|
self.stroke.hash(state);
|
||||||
self.fill.hash(state);
|
self.fill.hash(state);
|
||||||
self.alignment.hash(state);
|
self.alignment.hash(state);
|
||||||
self.path.hash(state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Font {
|
pub struct Font {
|
||||||
name: String,
|
name: String,
|
||||||
weight: Weight,
|
weight: Weight,
|
||||||
|
|
@ -78,38 +43,11 @@ pub struct Font {
|
||||||
size: u8,
|
size: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
impl From<iced::font::Font> for Font {
|
||||||
Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize,
|
fn from(value: iced::font::Font) -> Self {
|
||||||
)]
|
|
||||||
pub struct Shadow {
|
|
||||||
pub offset_x: i16,
|
|
||||||
pub offset_y: i16,
|
|
||||||
pub spread: u16,
|
|
||||||
pub color: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Stroke {
|
|
||||||
size: u16,
|
|
||||||
color: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Message {
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct Color(Srgb);
|
|
||||||
|
|
||||||
impl From<cosmic::font::Font> for Font {
|
|
||||||
fn from(value: cosmic::font::Font) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
name: match value.family {
|
name: match value.family {
|
||||||
cosmic::iced::font::Family::Name(name) => {
|
iced::font::Family::Name(name) => name.to_string(),
|
||||||
name.to_string()
|
|
||||||
}
|
|
||||||
_ => "Quicksand Bold".into(),
|
_ => "Quicksand Bold".into(),
|
||||||
},
|
},
|
||||||
size: 20,
|
size: 20,
|
||||||
|
|
@ -137,100 +75,72 @@ impl From<&str> for Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Font {
|
impl Font {
|
||||||
#[must_use]
|
|
||||||
pub fn get_name(&self) -> String {
|
pub fn get_name(&self) -> String {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn get_weight(&self) -> Weight {
|
||||||
pub const fn get_weight(&self) -> Weight {
|
|
||||||
self.weight
|
self.weight
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn get_style(&self) -> Style {
|
||||||
pub const fn get_style(&self) -> Style {
|
|
||||||
self.style
|
self.style
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn weight(mut self, weight: impl Into<Weight>) -> Self {
|
pub fn weight(mut self, weight: impl Into<Weight>) -> Self {
|
||||||
self.weight = weight.into();
|
self.weight = weight.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn style(mut self, style: impl Into<Style>) -> Self {
|
pub fn style(mut self, style: impl Into<Style>) -> Self {
|
||||||
self.style = style.into();
|
self.style = style.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn name(mut self, name: impl Into<String>) -> Self {
|
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||||
self.name = name.into();
|
self.name = name.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn size(mut self, size: u8) -> Self {
|
||||||
pub const fn size(mut self, size: u8) -> Self {
|
|
||||||
self.size = size;
|
self.size = size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Color(Rgb);
|
||||||
|
|
||||||
impl Hash for Color {
|
impl Hash for Color {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.to_css_hex_string().hash(state);
|
self.0.to_css_hex_string().hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Color {
|
impl Color {
|
||||||
#[must_use]
|
pub fn from_hex_str(color: impl AsRef<str>) -> Color {
|
||||||
pub fn to_css_hex_string(&self) -> String {
|
match Rgb::from_hex_str(color.as_ref()) {
|
||||||
format!("#{:x}", self.0.into_format::<u8>())
|
Ok(rgb) => Color(rgb),
|
||||||
}
|
|
||||||
|
|
||||||
#[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();
|
|
||||||
match color {
|
|
||||||
Ok(srgb) => Self(srgb.into()),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("error in making color from hex_str: {:?}", e);
|
error!("error in making color from hex_str: {:?}", e);
|
||||||
Self::default()
|
Color::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
impl From<&str> for Color {
|
||||||
fn from(value: &str) -> Self {
|
fn from(value: &str) -> Self {
|
||||||
Self::from_hex_str(value)
|
Self::from_hex_str(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Color {
|
|
||||||
fn from(value: String) -> Self {
|
|
||||||
Self::from_hex_str(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Color {
|
impl Default for Color {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Srgb::new(0.0, 0.0, 0.0))
|
Self(
|
||||||
|
Rgb::from_hex_str("#000")
|
||||||
|
.expect("This is not a hex color"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,12 +149,29 @@ impl Display for Color {
|
||||||
&self,
|
&self,
|
||||||
f: &mut std::fmt::Formatter<'_>,
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
) -> std::fmt::Result {
|
) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.to_css_hex_string())
|
write!(f, "{}", self.0.to_css_hex_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Hash)]
|
||||||
|
pub struct Shadow {
|
||||||
|
pub offset_x: i16,
|
||||||
|
pub offset_y: i16,
|
||||||
|
pub spread: u16,
|
||||||
|
pub color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Hash)]
|
||||||
|
pub struct Stroke {
|
||||||
|
size: u16,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Message {
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
impl TextSvg {
|
impl TextSvg {
|
||||||
#[must_use]
|
|
||||||
pub fn new(text: impl Into<String>) -> Self {
|
pub fn new(text: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
|
|
@ -254,279 +181,114 @@ impl TextSvg {
|
||||||
|
|
||||||
// pub fn build(self)
|
// pub fn build(self)
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn fill(mut self, color: impl Into<Color>) -> Self {
|
pub fn fill(mut self, color: impl Into<Color>) -> Self {
|
||||||
self.fill = color.into();
|
self.fill = color.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn shadow(mut self, shadow: impl Into<Shadow>) -> Self {
|
pub fn shadow(mut self, shadow: impl Into<Shadow>) -> Self {
|
||||||
self.shadow = Some(shadow.into());
|
self.shadow = Some(shadow.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||||
self.stroke = Some(stroke.into());
|
self.stroke = Some(stroke.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn font(mut self, font: impl Into<Font>) -> Self {
|
pub fn font(mut self, font: impl Into<Font>) -> Self {
|
||||||
self.font = font.into();
|
self.font = font.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn text(mut self, text: impl AsRef<str>) -> Self {
|
pub fn text(mut self, text: impl AsRef<str>) -> Self {
|
||||||
self.text = text.as_ref().to_string();
|
self.text = text.as_ref().to_string();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn alignment(mut self, alignment: TextAlignment) -> Self {
|
||||||
pub fn fontdb(mut self, fontdb: Arc<fontdb::Database>) -> Self {
|
|
||||||
self.fontdb = fontdb;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn alignment(
|
|
||||||
mut self,
|
|
||||||
alignment: TextAlignment,
|
|
||||||
) -> Self {
|
|
||||||
self.alignment = alignment;
|
self.alignment = alignment;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn build(mut self) -> Self {
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
let shadow = if let Some(shadow) = &self.shadow {
|
||||||
#[allow(clippy::cast_precision_loss)]
|
format!("<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn build(
|
|
||||||
mut self,
|
|
||||||
size: Size,
|
|
||||||
mut cache: Option<PathBuf>,
|
|
||||||
) -> Self {
|
|
||||||
// debug!("starting...");
|
|
||||||
|
|
||||||
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 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())
|
|
||||||
}
|
|
||||||
TextAlignment::MiddleLeft => {
|
|
||||||
let middle_position = size.height / 2.0;
|
|
||||||
let position = half_lines.mul_add(
|
|
||||||
-text_and_line_spacing,
|
|
||||||
middle_position,
|
|
||||||
);
|
|
||||||
("start", position, "10")
|
|
||||||
}
|
|
||||||
TextAlignment::MiddleCenter => {
|
|
||||||
let middle_position = size.height / 2.0;
|
|
||||||
let position = half_lines.mul_add(
|
|
||||||
-text_and_line_spacing,
|
|
||||||
middle_position,
|
|
||||||
);
|
|
||||||
("middle", position, center_y.as_str())
|
|
||||||
}
|
|
||||||
TextAlignment::MiddleRight => {
|
|
||||||
let middle_position = size.height / 2.0;
|
|
||||||
let position = half_lines.mul_add(
|
|
||||||
-text_and_line_spacing,
|
|
||||||
middle_position,
|
|
||||||
);
|
|
||||||
("end", position, x_width_padded.as_str())
|
|
||||||
}
|
|
||||||
TextAlignment::BottomLeft => {
|
|
||||||
let position = (total_lines as f32)
|
|
||||||
.mul_add(-text_and_line_spacing, size.height);
|
|
||||||
("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())
|
|
||||||
}
|
|
||||||
TextAlignment::BottomRight => {
|
|
||||||
let position = (total_lines as f32)
|
|
||||||
.mul_add(-text_and_line_spacing, size.height);
|
|
||||||
("end", position, x_width_padded.as_str())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(shadow) = &self.shadow {
|
|
||||||
let _ = write!(
|
|
||||||
final_svg,
|
|
||||||
"<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
|
||||||
shadow.offset_x,
|
shadow.offset_x,
|
||||||
shadow.offset_y,
|
shadow.offset_y,
|
||||||
shadow.spread,
|
shadow.spread,
|
||||||
shadow.color
|
shadow.color)
|
||||||
);
|
} else {
|
||||||
}
|
"".into()
|
||||||
final_svg.push_str("</defs>");
|
};
|
||||||
|
let stroke = if let Some(stroke) = &self.stroke {
|
||||||
// This would be how to apply kerning
|
format!(
|
||||||
// final_svg.push_str(
|
|
||||||
// "<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
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(stroke) = &self.stroke {
|
|
||||||
let _ = write!(
|
|
||||||
final_svg,
|
|
||||||
"stroke=\"{}\" stroke-width=\"{}px\" stroke-linejoin=\"arcs\" paint-order=\"stroke\"",
|
"stroke=\"{}\" stroke-width=\"{}px\" stroke-linejoin=\"arcs\" paint-order=\"stroke\"",
|
||||||
stroke.color, stroke.size
|
stroke.color, stroke.size
|
||||||
);
|
)
|
||||||
}
|
} else {
|
||||||
|
"".into()
|
||||||
|
};
|
||||||
|
let size = Size::new(640.0, 360.0);
|
||||||
|
let total_lines = self.text.lines().count();
|
||||||
|
let half_lines = (total_lines / 2) as f32;
|
||||||
|
let middle_position = size.height / 2.0;
|
||||||
|
let line_spacing = 10.0;
|
||||||
|
let text_and_line_spacing =
|
||||||
|
self.font.size as f32 + line_spacing;
|
||||||
|
let starting_y_position =
|
||||||
|
middle_position - (half_lines * text_and_line_spacing);
|
||||||
|
|
||||||
if self.shadow.is_some() {
|
let text_pieces: Vec<String> = self
|
||||||
final_svg.push_str(" style=\"filter:url(#shadow);\"");
|
.text
|
||||||
}
|
.lines()
|
||||||
final_svg.push('>');
|
.enumerate()
|
||||||
|
.map(|(index, text)| {
|
||||||
for (index, text) in self.text.lines().enumerate() {
|
format!(
|
||||||
let _ = write!(
|
"<tspan x=\"50%\" y=\"{}\">{}</tspan>",
|
||||||
final_svg,
|
|
||||||
"<tspan x=\"0\" y=\"{}\">{}</tspan>",
|
|
||||||
(index as f32).mul_add(
|
|
||||||
text_and_line_spacing,
|
|
||||||
starting_y_position
|
starting_y_position
|
||||||
),
|
+ (index as f32 * text_and_line_spacing),
|
||||||
text
|
text
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let text: String = text_pieces.join("\n");
|
||||||
|
|
||||||
|
let final_svg = format!("<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\"><defs>{}</defs><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-weight=\"bold\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\" {} style=\"filter:url(#shadow);\">{}</text></svg>",
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
shadow,
|
||||||
|
self.font.name,
|
||||||
|
self.font.size,
|
||||||
|
self.fill, stroke, text);
|
||||||
|
let handle = Handle::from_memory(
|
||||||
|
Box::leak(
|
||||||
|
<std::string::String as Clone>::clone(&final_svg)
|
||||||
|
.into_boxed_str(),
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
final_svg.push_str("</text></svg>");
|
|
||||||
|
|
||||||
// final_svg.push_str(&format!(
|
|
||||||
// "<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\"><defs>{}</defs><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-weight=\"bold\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\" {} style=\"filter:url(#shadow);\">{}</text></svg>",
|
|
||||||
// size.width,
|
|
||||||
// size.height,
|
|
||||||
// shadow,
|
|
||||||
// self.font.name,
|
|
||||||
// font_size,
|
|
||||||
// self.fill,
|
|
||||||
// stroke,
|
|
||||||
// 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");
|
|
||||||
|
|
||||||
if path.exists() {
|
|
||||||
// debug!("cached");
|
|
||||||
let handle = Handle::from_path(&path);
|
|
||||||
self.path = Some(path.clone());
|
|
||||||
self.handle = Some(handle);
|
self.handle = Some(handle);
|
||||||
return self;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// debug!("text string built...");
|
|
||||||
let Ok(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;
|
|
||||||
};
|
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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());
|
|
||||||
self.handle = Some(handle);
|
|
||||||
// debug!("stored");
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view<'a>(&self) -> Element<'a, Message> {
|
pub fn view<'a>(&self) -> Element<'a, Message> {
|
||||||
self.handle.clone().map_or_else(
|
container(
|
||||||
|| Element::from(Space::new(Length::Fill, Length::Fill)),
|
Svg::new(self.handle.clone().unwrap())
|
||||||
|handle| {
|
.width(Length::Fill)
|
||||||
Image::new(handle)
|
.height(Length::Fill),
|
||||||
.content_fit(ContentFit::Cover)
|
)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
fn text_spans(&self) -> Vec<String> {
|
||||||
|
self.text
|
||||||
|
.lines()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, t)| format!("<tspan x=\"50%\">{}</tspan>", t))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,92 +317,26 @@ pub fn color(color: impl AsRef<str>) -> Color {
|
||||||
Color::from_hex_str(color)
|
Color::from_hex_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();
|
|
||||||
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);
|
|
||||||
slide.text_svg = Some(text_svg);
|
|
||||||
Ok(slide)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod test {
|
||||||
use crate::core::slide::Slide;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::TextSvg;
|
||||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
|
||||||
use resvg::usvg::fontdb::Database;
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generator() {
|
fn test_text_spans() {
|
||||||
let slide = Slide::default();
|
let mut text = TextSvg::new("yes");
|
||||||
debug!("test");
|
text.text = "This is
|
||||||
let mut fontdb = Database::new();
|
multiline
|
||||||
fontdb.load_system_fonts();
|
text."
|
||||||
let fontdb = Arc::new(fontdb);
|
.into();
|
||||||
(0..400).into_par_iter().for_each(|_| {
|
assert_eq!(
|
||||||
let slide = slide
|
vec![
|
||||||
.clone()
|
String::from("<tspan>This is</tspan>"),
|
||||||
.set_font_size(120)
|
String::from("<tspan>multiline</tspan>"),
|
||||||
.set_font("")
|
String::from("<tspan>text.</tspan>"),
|
||||||
.set_shadow(shadow(5, 5, 5, "#000"))
|
],
|
||||||
.set_stroke(stroke(9, "#000"))
|
text.text_spans()
|
||||||
.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}"),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,237 +0,0 @@
|
||||||
use std::{io, path::PathBuf};
|
|
||||||
|
|
||||||
use cosmic::{
|
|
||||||
Element, Task,
|
|
||||||
dialog::file_chooser::{FileFilter, open::Dialog},
|
|
||||||
iced::{Length, alignment::Vertical},
|
|
||||||
iced_widget::{column, row},
|
|
||||||
theme,
|
|
||||||
widget::{
|
|
||||||
Space, button, container, horizontal_space, icon,
|
|
||||||
progress_bar, text, text_input,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use iced_video_player::{Video, VideoPlayer};
|
|
||||||
use tracing::{debug, error, warn};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::core::videos;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct VideoEditor {
|
|
||||||
pub video: Option<Video>,
|
|
||||||
core_video: Option<videos::Video>,
|
|
||||||
title: String,
|
|
||||||
editing: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Action {
|
|
||||||
Task(Task<Message>),
|
|
||||||
UpdateVideo(videos::Video),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Message {
|
|
||||||
ChangeVideo(videos::Video),
|
|
||||||
Update(videos::Video),
|
|
||||||
ChangeTitle(String),
|
|
||||||
PickVideo,
|
|
||||||
Edit(bool),
|
|
||||||
None,
|
|
||||||
PauseVideo,
|
|
||||||
UpdateVideoFile(videos::Video),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VideoEditor {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
video: None,
|
|
||||||
core_video: None,
|
|
||||||
title: "Death was Arrested".to_string(),
|
|
||||||
editing: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn update(&mut self, message: Message) -> Action {
|
|
||||||
match message {
|
|
||||||
Message::ChangeVideo(video) => {
|
|
||||||
self.update_entire_video(&video);
|
|
||||||
}
|
|
||||||
Message::ChangeTitle(title) => {
|
|
||||||
self.title.clone_from(&title);
|
|
||||||
if let Some(video) = &self.core_video {
|
|
||||||
let mut video = video.clone();
|
|
||||||
video.title = title;
|
|
||||||
return self.update(Message::Update(video));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::Edit(edit) => {
|
|
||||||
debug!(edit);
|
|
||||||
self.editing = edit;
|
|
||||||
}
|
|
||||||
Message::PauseVideo => {
|
|
||||||
if let Some(video) = &mut self.video {
|
|
||||||
let paused = video.paused();
|
|
||||||
video.set_paused(!paused);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::Update(video) => {
|
|
||||||
warn!(?video);
|
|
||||||
return Action::UpdateVideo(video);
|
|
||||||
}
|
|
||||||
Message::PickVideo => {
|
|
||||||
let video_id = self
|
|
||||||
.core_video
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.id)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let task = Task::perform(
|
|
||||||
pick_video(),
|
|
||||||
move |video_result| {
|
|
||||||
video_result.map_or(Message::None, |video| {
|
|
||||||
let mut video =
|
|
||||||
videos::Video::from(video);
|
|
||||||
video.id = video_id;
|
|
||||||
Message::UpdateVideoFile(video)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Action::Task(task);
|
|
||||||
}
|
|
||||||
Message::UpdateVideoFile(video) => {
|
|
||||||
self.update_entire_video(&video);
|
|
||||||
return Action::UpdateVideo(video);
|
|
||||||
}
|
|
||||||
Message::None => (),
|
|
||||||
}
|
|
||||||
Action::None
|
|
||||||
}
|
|
||||||
|
|
||||||
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_player = self.video.as_ref().map_or_else(
|
|
||||||
|| 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);
|
|
||||||
let column = column![
|
|
||||||
self.toolbar(),
|
|
||||||
container(video_section).center_x(Length::FillPortion(2))
|
|
||||||
]
|
|
||||||
.spacing(theme::active().cosmic().space_l());
|
|
||||||
column.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toolbar(&self) -> Element<Message> {
|
|
||||||
let title_box = text_input("Title...", &self.title)
|
|
||||||
.on_input(Message::ChangeTitle);
|
|
||||||
|
|
||||||
let video_selector = button::icon(
|
|
||||||
icon::from_name("folder-videos-symbolic").scale(2),
|
|
||||||
)
|
|
||||||
.label("Video")
|
|
||||||
.tooltip("Select a video")
|
|
||||||
.on_press(Message::PickVideo)
|
|
||||||
.padding(10);
|
|
||||||
|
|
||||||
row![
|
|
||||||
text::body("Title:"),
|
|
||||||
title_box,
|
|
||||||
horizontal_space(),
|
|
||||||
video_selector
|
|
||||||
]
|
|
||||||
.align_y(Vertical::Center)
|
|
||||||
.spacing(10)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn editing(&self) -> bool {
|
|
||||||
self.editing
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_entire_video(&mut self, video: &videos::Video) {
|
|
||||||
let Ok(mut player_video) =
|
|
||||||
Url::from_file_path(video.path.clone())
|
|
||||||
.map(|url| Video::new(&url).expect("Should be here"))
|
|
||||||
else {
|
|
||||||
self.video = None;
|
|
||||||
self.title.clone_from(&video.title);
|
|
||||||
self.core_video = Some(video.clone());
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
player_video.set_paused(true);
|
|
||||||
self.video = Some(player_video);
|
|
||||||
self.title.clone_from(&video.title);
|
|
||||||
self.core_video = Some(video.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VideoEditor {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pick_video() -> Result<PathBuf, VideoError> {
|
|
||||||
let dialog = Dialog::new().title("Choose a video...");
|
|
||||||
let bg_filter = FileFilter::new("Videos")
|
|
||||||
.extension("mp4")
|
|
||||||
.extension("webm")
|
|
||||||
.extension("mkv");
|
|
||||||
dialog
|
|
||||||
.filter(bg_filter)
|
|
||||||
.directory(dirs::home_dir().expect("oops"))
|
|
||||||
.open_file()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!(?e);
|
|
||||||
VideoError::DialogClosed
|
|
||||||
})
|
|
||||||
.map(|file| {
|
|
||||||
file.url().to_file_path().expect("Should be a file here")
|
|
||||||
})
|
|
||||||
// rfd::AsyncFileDialog::new()
|
|
||||||
// .set_title("Choose a background...")
|
|
||||||
// .add_filter(
|
|
||||||
// "Images and Videos",
|
|
||||||
// &["png", "jpeg", "mp4", "webm", "mkv", "jpg", "mpeg"],
|
|
||||||
// )
|
|
||||||
// .set_directory(dirs::home_dir().unwrap())
|
|
||||||
// .pick_file()
|
|
||||||
// .await
|
|
||||||
// .ok_or(VideoError::BackgroundDialogClosed)
|
|
||||||
// .map(|file| file.path().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum VideoError {
|
|
||||||
DialogClosed,
|
|
||||||
IOError(io::ErrorKind),
|
|
||||||
}
|
|
||||||
|
|
@ -1,914 +0,0 @@
|
||||||
//! Distribute draggable content vertically.
|
|
||||||
// This widget is a modification of the original `Column` widget from [`iced`]
|
|
||||||
//
|
|
||||||
// [`iced`]: https://github.com/iced-rs/iced
|
|
||||||
//
|
|
||||||
// Copyright 2019 Héctor Ramón, Iced contributors
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
// this software and associated documentation files (the "Software"), to deal in
|
|
||||||
// the Software without restriction, including without limitation the rights to
|
|
||||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
// subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
use cosmic::Theme;
|
|
||||||
use cosmic::iced::advanced::layout::{self, Layout};
|
|
||||||
use cosmic::iced::advanced::widget::{Operation, Tree, Widget, tree};
|
|
||||||
use cosmic::iced::advanced::{Clipboard, Shell, overlay, renderer};
|
|
||||||
use cosmic::iced::alignment::{self, Alignment};
|
|
||||||
use cosmic::iced::event::{self, Event};
|
|
||||||
use cosmic::iced::{self, Transformation, mouse};
|
|
||||||
use cosmic::iced::{
|
|
||||||
Background, Border, Color, Element, Length, Padding, Pixels,
|
|
||||||
Point, Rectangle, Size, Vector,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{Action, DragEvent, DropPosition};
|
|
||||||
|
|
||||||
pub fn column<'a, Message, Theme, Renderer>(
|
|
||||||
children: impl IntoIterator<
|
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Column<'a, Message, Theme, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: renderer::Renderer,
|
|
||||||
Theme: Catalog,
|
|
||||||
{
|
|
||||||
Column::with_children(children)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DRAG_DEADBAND_DISTANCE: f32 = 5.0;
|
|
||||||
|
|
||||||
/// A container that distributes its contents vertically.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```no_run
|
|
||||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
|
||||||
/// # pub type State = ();
|
|
||||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
|
||||||
/// use iced::widget::{button, column};
|
|
||||||
///
|
|
||||||
/// #[derive(Debug, Clone)]
|
|
||||||
/// enum Message {
|
|
||||||
/// // ...
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// fn view(state: &State) -> Element<'_, Message> {
|
|
||||||
/// column![
|
|
||||||
/// "I am on top!",
|
|
||||||
/// button("I am in the center!"),
|
|
||||||
/// "I am below.",
|
|
||||||
/// ].into()
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
#[allow(missing_debug_implementations)]
|
|
||||||
pub struct Column<
|
|
||||||
'a,
|
|
||||||
Message,
|
|
||||||
Theme = cosmic::Theme,
|
|
||||||
Renderer = iced::Renderer,
|
|
||||||
> where
|
|
||||||
Theme: Catalog,
|
|
||||||
{
|
|
||||||
spacing: f32,
|
|
||||||
padding: Padding,
|
|
||||||
width: Length,
|
|
||||||
height: Length,
|
|
||||||
max_width: f32,
|
|
||||||
align: Alignment,
|
|
||||||
clip: bool,
|
|
||||||
deadband_zone: f32,
|
|
||||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
|
||||||
on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
|
||||||
class: Theme::Class<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer>
|
|
||||||
Column<'a, Message, Theme, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: renderer::Renderer,
|
|
||||||
Theme: Catalog,
|
|
||||||
{
|
|
||||||
/// Creates an empty [`Column`].
|
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::from_vec(Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [`Column`] with the given capacity.
|
|
||||||
#[must_use]
|
|
||||||
pub fn with_capacity(capacity: usize) -> Self {
|
|
||||||
Self::from_vec(Vec::with_capacity(capacity))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [`Column`] with the given elements.
|
|
||||||
pub fn with_children(
|
|
||||||
children: impl IntoIterator<
|
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
|
||||||
let iterator = children.into_iter();
|
|
||||||
|
|
||||||
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [`Column`] from an already allocated [`Vec`].
|
|
||||||
///
|
|
||||||
/// Keep in mind that the [`Column`] will not inspect the [`Vec`], which means
|
|
||||||
/// it won't automatically adapt to the sizing strategy of its contents.
|
|
||||||
///
|
|
||||||
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
|
||||||
/// call [`Column::width`] or [`Column::height`] accordingly.
|
|
||||||
#[must_use]
|
|
||||||
pub fn from_vec(
|
|
||||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
spacing: 0.0,
|
|
||||||
padding: Padding::ZERO,
|
|
||||||
width: Length::Shrink,
|
|
||||||
height: Length::Shrink,
|
|
||||||
max_width: f32::INFINITY,
|
|
||||||
align: Alignment::Start,
|
|
||||||
clip: false,
|
|
||||||
deadband_zone: DRAG_DEADBAND_DISTANCE,
|
|
||||||
children,
|
|
||||||
class: Theme::default(),
|
|
||||||
on_drag: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the vertical spacing _between_ elements.
|
|
||||||
///
|
|
||||||
/// Custom margins per element do not exist in iced. You should use this
|
|
||||||
/// method instead! While less flexible, it helps you keep spacing between
|
|
||||||
/// elements consistent.
|
|
||||||
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
|
||||||
self.spacing = amount.into().0;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the [`Padding`] of the [`Column`].
|
|
||||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
|
||||||
self.padding = padding.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the width of the [`Column`].
|
|
||||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
|
||||||
self.width = width.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the height of the [`Column`].
|
|
||||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
|
||||||
self.height = height.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the maximum width of the [`Column`].
|
|
||||||
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
|
||||||
self.max_width = max_width.into().0;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
|
||||||
pub fn align_x(
|
|
||||||
mut self,
|
|
||||||
align: impl Into<alignment::Horizontal>,
|
|
||||||
) -> Self {
|
|
||||||
self.align = Alignment::from(align.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets whether the contents of the [`Column`] should be clipped on
|
|
||||||
/// overflow.
|
|
||||||
pub const fn clip(mut self, clip: bool) -> Self {
|
|
||||||
self.clip = clip;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the drag deadband zone of the [`Column`].
|
|
||||||
pub const fn deadband_zone(mut self, deadband_zone: f32) -> Self {
|
|
||||||
self.deadband_zone = deadband_zone;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds an element to the [`Column`].
|
|
||||||
pub fn push(
|
|
||||||
mut self,
|
|
||||||
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
|
||||||
) -> Self {
|
|
||||||
let child = child.into();
|
|
||||||
let child_size = child.as_widget().size_hint();
|
|
||||||
|
|
||||||
self.width = self.width.enclose(child_size.width);
|
|
||||||
self.height = self.height.enclose(child_size.height);
|
|
||||||
|
|
||||||
self.children.push(child);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds an element to the [`Column`], if `Some`.
|
|
||||||
pub fn push_maybe(
|
|
||||||
self,
|
|
||||||
child: Option<
|
|
||||||
impl Into<Element<'a, Message, Theme, Renderer>>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
|
||||||
if let Some(child) = child {
|
|
||||||
self.push(child)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the style of the [`Column`].
|
|
||||||
#[must_use]
|
|
||||||
pub fn style(
|
|
||||||
mut self,
|
|
||||||
style: impl Fn(&Theme) -> Style + 'a,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
|
||||||
{
|
|
||||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the style class of the [`Column`].
|
|
||||||
#[must_use]
|
|
||||||
pub fn class(
|
|
||||||
mut self,
|
|
||||||
class: impl Into<Theme::Class<'a>>,
|
|
||||||
) -> Self {
|
|
||||||
self.class = class.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extends the [`Column`] with the given children.
|
|
||||||
pub fn extend(
|
|
||||||
self,
|
|
||||||
children: impl IntoIterator<
|
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
|
||||||
children.into_iter().fold(self, Self::push)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The message produced by the [`Column`] when a child is dragged.
|
|
||||||
pub fn on_drag(
|
|
||||||
mut self,
|
|
||||||
on_reorder: impl Fn(DragEvent) -> Message + 'a,
|
|
||||||
) -> Self {
|
|
||||||
self.on_drag = Some(Box::new(on_reorder));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computes the index and position where a dragged item should be dropped.
|
|
||||||
fn compute_target_index(
|
|
||||||
&self,
|
|
||||||
cursor_position: Point,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
dragged_index: usize,
|
|
||||||
) -> (usize, DropPosition) {
|
|
||||||
let cursor_y = cursor_position.y;
|
|
||||||
|
|
||||||
for (i, child_layout) in layout.children().enumerate() {
|
|
||||||
let bounds = child_layout.bounds();
|
|
||||||
let y = bounds.y;
|
|
||||||
let height = bounds.height;
|
|
||||||
|
|
||||||
if cursor_y >= y && cursor_y <= y + height {
|
|
||||||
if i == dragged_index {
|
|
||||||
// Cursor is over the dragged item itself
|
|
||||||
return (i, DropPosition::Swap);
|
|
||||||
}
|
|
||||||
|
|
||||||
let thickness = height / 4.0;
|
|
||||||
let top_threshold = y + thickness;
|
|
||||||
let bottom_threshold = y + height - thickness;
|
|
||||||
|
|
||||||
if cursor_y < top_threshold {
|
|
||||||
// Near the top edge - insert above
|
|
||||||
return (i, DropPosition::Before);
|
|
||||||
} else if cursor_y > bottom_threshold {
|
|
||||||
// Near the bottom edge - insert below
|
|
||||||
return (i + 1, DropPosition::After);
|
|
||||||
}
|
|
||||||
// Middle area - swap
|
|
||||||
return (i, DropPosition::Swap);
|
|
||||||
} else if cursor_y < y {
|
|
||||||
// Cursor is above this child
|
|
||||||
return (i, DropPosition::Before);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor is below all children
|
|
||||||
(self.children.len(), DropPosition::After)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Message, Renderer> Default
|
|
||||||
for Column<'_, Message, Theme, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: renderer::Renderer,
|
|
||||||
Theme: Catalog,
|
|
||||||
{
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer: renderer::Renderer>
|
|
||||||
FromIterator<Element<'a, Message, Theme, Renderer>>
|
|
||||||
for Column<'a, Message, Theme, Renderer>
|
|
||||||
where
|
|
||||||
Theme: Catalog,
|
|
||||||
{
|
|
||||||
fn from_iter<
|
|
||||||
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
|
||||||
>(
|
|
||||||
iter: T,
|
|
||||||
) -> Self {
|
|
||||||
Self::with_children(iter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
|
||||||
for Column<'_, Message, Theme, Renderer>
|
|
||||||
where
|
|
||||||
Renderer: renderer::Renderer,
|
|
||||||
Theme: Catalog,
|
|
||||||
{
|
|
||||||
fn tag(&self) -> tree::Tag {
|
|
||||||
tree::Tag::of::<Action>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> tree::State {
|
|
||||||
tree::State::new(Action::Idle)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn children(&self) -> Vec<Tree> {
|
|
||||||
self.children.iter().map(Tree::new).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diff(&mut self, tree: &mut Tree) {
|
|
||||||
tree.diff_children(self.children.as_mut_slice());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size(&self) -> Size<Length> {
|
|
||||||
Size {
|
|
||||||
width: self.width,
|
|
||||||
height: self.height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(
|
|
||||||
&self,
|
|
||||||
tree: &mut Tree,
|
|
||||||
renderer: &Renderer,
|
|
||||||
limits: &layout::Limits,
|
|
||||||
) -> layout::Node {
|
|
||||||
let limits = limits.max_width(self.max_width);
|
|
||||||
|
|
||||||
layout::flex::resolve(
|
|
||||||
layout::flex::Axis::Vertical,
|
|
||||||
renderer,
|
|
||||||
&limits,
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
self.padding,
|
|
||||||
self.spacing,
|
|
||||||
self.align,
|
|
||||||
&self.children,
|
|
||||||
&mut tree.children,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn operate(
|
|
||||||
&self,
|
|
||||||
tree: &mut Tree,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
renderer: &Renderer,
|
|
||||||
operation: &mut dyn Operation,
|
|
||||||
) {
|
|
||||||
operation.container(
|
|
||||||
None,
|
|
||||||
layout.bounds(),
|
|
||||||
&mut |operation| {
|
|
||||||
self.children
|
|
||||||
.iter()
|
|
||||||
.zip(&mut tree.children)
|
|
||||||
.zip(layout.children())
|
|
||||||
.for_each(|((child, state), c_layout)| {
|
|
||||||
child.as_widget().operate(
|
|
||||||
state,
|
|
||||||
c_layout.with_virtual_offset(
|
|
||||||
layout.virtual_offset(),
|
|
||||||
),
|
|
||||||
renderer,
|
|
||||||
operation,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_event(
|
|
||||||
&mut self,
|
|
||||||
tree: &mut Tree,
|
|
||||||
event: Event,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
cursor: mouse::Cursor,
|
|
||||||
renderer: &Renderer,
|
|
||||||
clipboard: &mut dyn Clipboard,
|
|
||||||
shell: &mut Shell<'_, Message>,
|
|
||||||
viewport: &Rectangle,
|
|
||||||
) -> event::Status {
|
|
||||||
let mut event_status = event::Status::Ignored;
|
|
||||||
|
|
||||||
let action = tree.state.downcast_mut::<Action>();
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Event::Mouse(mouse::Event::ButtonPressed(
|
|
||||||
mouse::Button::Left,
|
|
||||||
)) => {
|
|
||||||
if let Some(cursor_position) =
|
|
||||||
cursor.position_over(layout.bounds())
|
|
||||||
{
|
|
||||||
for (index, child_layout) in
|
|
||||||
layout.children().enumerate()
|
|
||||||
{
|
|
||||||
if child_layout
|
|
||||||
.bounds()
|
|
||||||
.contains(cursor_position)
|
|
||||||
{
|
|
||||||
*action = Action::Picking {
|
|
||||||
index,
|
|
||||||
origin: cursor_position,
|
|
||||||
};
|
|
||||||
event_status = event::Status::Captured;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
|
||||||
match *action {
|
|
||||||
Action::Picking { index, origin } => {
|
|
||||||
if let Some(cursor_position) =
|
|
||||||
cursor.position()
|
|
||||||
&& cursor_position.distance(origin)
|
|
||||||
> self.deadband_zone
|
|
||||||
{
|
|
||||||
// Start dragging
|
|
||||||
*action = Action::Dragging {
|
|
||||||
index,
|
|
||||||
origin,
|
|
||||||
last_cursor: cursor_position,
|
|
||||||
};
|
|
||||||
if let Some(on_reorder) = &self.on_drag {
|
|
||||||
shell.publish(on_reorder(
|
|
||||||
DragEvent::Picked { index },
|
|
||||||
));
|
|
||||||
}
|
|
||||||
event_status = event::Status::Captured;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Action::Dragging { origin, index, .. } => {
|
|
||||||
if let Some(cursor_position) =
|
|
||||||
cursor.position()
|
|
||||||
{
|
|
||||||
*action = Action::Dragging {
|
|
||||||
last_cursor: cursor_position,
|
|
||||||
origin,
|
|
||||||
index,
|
|
||||||
};
|
|
||||||
event_status = event::Status::Captured;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::Mouse(mouse::Event::ButtonReleased(
|
|
||||||
mouse::Button::Left,
|
|
||||||
)) => {
|
|
||||||
match *action {
|
|
||||||
Action::Dragging { index, .. } => {
|
|
||||||
if let Some(cursor_position) =
|
|
||||||
cursor.position()
|
|
||||||
{
|
|
||||||
let bounds = layout.bounds();
|
|
||||||
if bounds.contains(cursor_position) {
|
|
||||||
let (target_index, drop_position) =
|
|
||||||
self.compute_target_index(
|
|
||||||
cursor_position,
|
|
||||||
layout,
|
|
||||||
index,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(on_reorder) =
|
|
||||||
&self.on_drag
|
|
||||||
{
|
|
||||||
shell.publish(on_reorder(
|
|
||||||
DragEvent::Dropped {
|
|
||||||
index,
|
|
||||||
target_index,
|
|
||||||
drop_position,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
event_status =
|
|
||||||
event::Status::Captured;
|
|
||||||
}
|
|
||||||
} else if let Some(on_reorder) =
|
|
||||||
&self.on_drag
|
|
||||||
{
|
|
||||||
shell.publish(on_reorder(
|
|
||||||
DragEvent::Canceled { index },
|
|
||||||
));
|
|
||||||
event_status =
|
|
||||||
event::Status::Captured;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*action = Action::Idle;
|
|
||||||
}
|
|
||||||
Action::Picking { .. } => {
|
|
||||||
// Did not move enough to start dragging
|
|
||||||
*action = Action::Idle;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let child_status = self
|
|
||||||
.children
|
|
||||||
.iter_mut()
|
|
||||||
.zip(&mut tree.children)
|
|
||||||
.zip(layout.children())
|
|
||||||
.map(|((child, state), c_layout)| {
|
|
||||||
child.as_widget_mut().on_event(
|
|
||||||
state,
|
|
||||||
event.clone(),
|
|
||||||
c_layout
|
|
||||||
.with_virtual_offset(layout.virtual_offset()),
|
|
||||||
cursor,
|
|
||||||
renderer,
|
|
||||||
clipboard,
|
|
||||||
shell,
|
|
||||||
viewport,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.fold(event::Status::Ignored, event::Status::merge);
|
|
||||||
|
|
||||||
event::Status::merge(event_status, child_status)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mouse_interaction(
|
|
||||||
&self,
|
|
||||||
tree: &Tree,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
cursor: mouse::Cursor,
|
|
||||||
viewport: &Rectangle,
|
|
||||||
renderer: &Renderer,
|
|
||||||
) -> mouse::Interaction {
|
|
||||||
let action = tree.state.downcast_ref::<Action>();
|
|
||||||
|
|
||||||
if let Action::Dragging { .. } = *action {
|
|
||||||
return mouse::Interaction::Grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.children
|
|
||||||
.iter()
|
|
||||||
.zip(&tree.children)
|
|
||||||
.zip(layout.children())
|
|
||||||
.map(|((child, state), c_layout)| {
|
|
||||||
child.as_widget().mouse_interaction(
|
|
||||||
state,
|
|
||||||
c_layout
|
|
||||||
.with_virtual_offset(layout.virtual_offset()),
|
|
||||||
cursor,
|
|
||||||
viewport,
|
|
||||||
renderer,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.max()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
tree: &Tree,
|
|
||||||
renderer: &mut Renderer,
|
|
||||||
theme: &Theme,
|
|
||||||
default_style: &renderer::Style,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
cursor: mouse::Cursor,
|
|
||||||
viewport: &Rectangle,
|
|
||||||
) {
|
|
||||||
let action = tree.state.downcast_ref::<Action>();
|
|
||||||
let style = theme.style(&self.class);
|
|
||||||
|
|
||||||
match action {
|
|
||||||
Action::Dragging {
|
|
||||||
index,
|
|
||||||
last_cursor,
|
|
||||||
origin,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let child_count = self.children.len();
|
|
||||||
|
|
||||||
// Determine the target index based on cursor position
|
|
||||||
let target_index = if cursor.position().is_some() {
|
|
||||||
let (target_index, _) = self
|
|
||||||
.compute_target_index(
|
|
||||||
*last_cursor,
|
|
||||||
layout,
|
|
||||||
*index,
|
|
||||||
);
|
|
||||||
target_index.min(child_count - 1)
|
|
||||||
} else {
|
|
||||||
*index
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store the width of the dragged item
|
|
||||||
let drag_bounds =
|
|
||||||
layout.children().nth(*index).unwrap().bounds();
|
|
||||||
let drag_height = drag_bounds.height + self.spacing;
|
|
||||||
|
|
||||||
// Draw all children except the one being dragged
|
|
||||||
let mut translations = 0.0;
|
|
||||||
for i in 0..child_count {
|
|
||||||
let child = &self.children[i];
|
|
||||||
let state = &tree.children[i];
|
|
||||||
let child_layout =
|
|
||||||
layout.children().nth(i).unwrap();
|
|
||||||
|
|
||||||
// Draw the dragged item separately
|
|
||||||
// TODO: Draw a shadow below the picked item to enhance the
|
|
||||||
// floating effect
|
|
||||||
if i == *index {
|
|
||||||
let scaling =
|
|
||||||
Transformation::scale(style.scale);
|
|
||||||
let translation =
|
|
||||||
*last_cursor - *origin * scaling;
|
|
||||||
renderer.with_translation(
|
|
||||||
translation,
|
|
||||||
|renderer| {
|
|
||||||
renderer.with_transformation(
|
|
||||||
scaling,
|
|
||||||
|renderer| {
|
|
||||||
renderer.with_layer(
|
|
||||||
child_layout.bounds(),
|
|
||||||
|renderer| {
|
|
||||||
child
|
|
||||||
.as_widget()
|
|
||||||
.draw(
|
|
||||||
state,
|
|
||||||
renderer,
|
|
||||||
theme,
|
|
||||||
default_style,
|
|
||||||
child_layout,
|
|
||||||
cursor,
|
|
||||||
viewport,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let offset: i32 =
|
|
||||||
match target_index.cmp(index) {
|
|
||||||
std::cmp::Ordering::Less
|
|
||||||
if i >= target_index
|
|
||||||
&& i < *index =>
|
|
||||||
{
|
|
||||||
1
|
|
||||||
}
|
|
||||||
std::cmp::Ordering::Greater
|
|
||||||
if i > *index
|
|
||||||
&& i <= target_index =>
|
|
||||||
{
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let translation = Vector::new(
|
|
||||||
0.0,
|
|
||||||
offset as f32 * drag_height,
|
|
||||||
);
|
|
||||||
renderer.with_translation(
|
|
||||||
translation,
|
|
||||||
|renderer| {
|
|
||||||
child.as_widget().draw(
|
|
||||||
state,
|
|
||||||
renderer,
|
|
||||||
theme,
|
|
||||||
default_style,
|
|
||||||
child_layout,
|
|
||||||
cursor,
|
|
||||||
viewport,
|
|
||||||
);
|
|
||||||
// Draw an overlay if this item is being moved
|
|
||||||
// TODO: instead of drawing an overlay, it would be nicer to
|
|
||||||
// draw the item with a reduced opacity, but that's not possible today
|
|
||||||
if offset != 0 {
|
|
||||||
renderer.fill_quad(
|
|
||||||
renderer::Quad {
|
|
||||||
bounds: child_layout
|
|
||||||
.bounds(),
|
|
||||||
..renderer::Quad::default(
|
|
||||||
)
|
|
||||||
},
|
|
||||||
style.moved_item_overlay,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Keep track of the total translation so we can
|
|
||||||
// draw the "ghost" of the dragged item later
|
|
||||||
translations -= (child_layout
|
|
||||||
.bounds()
|
|
||||||
.height
|
|
||||||
+ self.spacing)
|
|
||||||
* offset.signum() as f32;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Draw a ghost of the dragged item in its would-be position
|
|
||||||
let ghost_translation =
|
|
||||||
Vector::new(0.0, translations);
|
|
||||||
renderer.with_translation(
|
|
||||||
ghost_translation,
|
|
||||||
|renderer| {
|
|
||||||
renderer.fill_quad(
|
|
||||||
renderer::Quad {
|
|
||||||
bounds: drag_bounds,
|
|
||||||
border: style.ghost_border,
|
|
||||||
..renderer::Quad::default()
|
|
||||||
},
|
|
||||||
style.ghost_background,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Draw all children normally when not dragging
|
|
||||||
if let Some(clipped_viewport) =
|
|
||||||
layout.bounds().intersection(viewport)
|
|
||||||
{
|
|
||||||
let viewport = if self.clip {
|
|
||||||
&clipped_viewport
|
|
||||||
} else {
|
|
||||||
viewport
|
|
||||||
};
|
|
||||||
for ((child, state), c_layout) in self
|
|
||||||
.children
|
|
||||||
.iter()
|
|
||||||
.zip(&tree.children)
|
|
||||||
.zip(layout.children())
|
|
||||||
.filter(|(_, layout)| {
|
|
||||||
layout.bounds().intersects(viewport)
|
|
||||||
})
|
|
||||||
{
|
|
||||||
child.as_widget().draw(
|
|
||||||
state,
|
|
||||||
renderer,
|
|
||||||
theme,
|
|
||||||
default_style,
|
|
||||||
c_layout.with_virtual_offset(
|
|
||||||
layout.virtual_offset(),
|
|
||||||
),
|
|
||||||
cursor,
|
|
||||||
viewport,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overlay<'b>(
|
|
||||||
&'b mut self,
|
|
||||||
tree: &'b mut Tree,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
renderer: &Renderer,
|
|
||||||
translation: Vector,
|
|
||||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
|
||||||
overlay::from_children(
|
|
||||||
&mut self.children,
|
|
||||||
tree,
|
|
||||||
layout,
|
|
||||||
renderer,
|
|
||||||
translation,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn drag_destinations(
|
|
||||||
&self,
|
|
||||||
state: &Tree,
|
|
||||||
layout: Layout<'_>,
|
|
||||||
renderer: &Renderer,
|
|
||||||
dnd_rectangles: &mut cosmic::iced_core::clipboard::DndDestinationRectangles,
|
|
||||||
) {
|
|
||||||
for ((e, c_layout), state) in self
|
|
||||||
.children
|
|
||||||
.iter()
|
|
||||||
.zip(layout.children())
|
|
||||||
.zip(state.children.iter())
|
|
||||||
{
|
|
||||||
e.as_widget().drag_destinations(
|
|
||||||
state,
|
|
||||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
|
||||||
renderer,
|
|
||||||
dnd_rectangles,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer>
|
|
||||||
From<Column<'a, Message, Theme, Renderer>>
|
|
||||||
for Element<'a, Message, Theme, Renderer>
|
|
||||||
where
|
|
||||||
Message: 'a,
|
|
||||||
Theme: Catalog + 'a,
|
|
||||||
Renderer: renderer::Renderer + 'a,
|
|
||||||
{
|
|
||||||
fn from(column: Column<'a, Message, Theme, Renderer>) -> Self {
|
|
||||||
Self::new(column)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The theme catalog of a [`Column`].
|
|
||||||
pub trait Catalog {
|
|
||||||
/// The item class of the [`Catalog`].
|
|
||||||
type Class<'a>;
|
|
||||||
|
|
||||||
/// The default class produced by the [`Catalog`].
|
|
||||||
fn default<'a>() -> Self::Class<'a>;
|
|
||||||
|
|
||||||
/// The [`Style`] of a class with the given status.
|
|
||||||
fn style(&self, class: &Self::Class<'_>) -> Style;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The appearance of a [`Column`].
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Style {
|
|
||||||
/// The scaling to apply to a picked element while it's being dragged.
|
|
||||||
pub scale: f32,
|
|
||||||
/// The color of the overlay on items that are moved around
|
|
||||||
pub moved_item_overlay: Color,
|
|
||||||
/// The outline border of the dragged item's ghost
|
|
||||||
pub ghost_border: Border,
|
|
||||||
/// The background of the dragged item's ghost
|
|
||||||
pub ghost_background: Background,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A styling function for a [`Column`].
|
|
||||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
|
||||||
|
|
||||||
impl Catalog for cosmic::Theme {
|
|
||||||
type Class<'a> = StyleFn<'a, Self>;
|
|
||||||
|
|
||||||
fn default<'a>() -> Self::Class<'a> {
|
|
||||||
Box::new(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style(&self, class: &Self::Class<'_>) -> Style {
|
|
||||||
class(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn default(theme: &Theme) -> Style {
|
|
||||||
Style {
|
|
||||||
scale: 1.05,
|
|
||||||
moved_item_overlay: Color::from(theme.cosmic().primary.base)
|
|
||||||
.scale_alpha(0.2),
|
|
||||||
ghost_border: Border {
|
|
||||||
width: 1.0,
|
|
||||||
color: theme.cosmic().secondary.base.into(),
|
|
||||||
radius: 0.0.into(),
|
|
||||||
},
|
|
||||||
ghost_background: Color::from(theme.cosmic().secondary.base)
|
|
||||||
.scale_alpha(0.2)
|
|
||||||
.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
use cosmic::iced::Point;
|
|
||||||
|
|
||||||
pub use self::column::column;
|
|
||||||
pub use self::row::row;
|
|
||||||
pub mod column;
|
|
||||||
pub mod row;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Action {
|
|
||||||
Idle,
|
|
||||||
Picking {
|
|
||||||
index: usize,
|
|
||||||
origin: Point,
|
|
||||||
},
|
|
||||||
Dragging {
|
|
||||||
index: usize,
|
|
||||||
origin: Point,
|
|
||||||
last_cursor: Point,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum DropPosition {
|
|
||||||
Before,
|
|
||||||
Swap,
|
|
||||||
After,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum DragEvent {
|
|
||||||
Picked {
|
|
||||||
index: usize,
|
|
||||||
},
|
|
||||||
Dropped {
|
|
||||||
index: usize,
|
|
||||||
target_index: usize,
|
|
||||||
drop_position: DropPosition,
|
|
||||||
},
|
|
||||||
Canceled {
|
|
||||||
index: usize,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
115
src/ui/widgets/icon/handle.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use super::{Icon, Named};
|
||||||
|
use iced::widget::{image, svg};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone, Debug, derive_setters::Setters)]
|
||||||
|
pub struct Handle {
|
||||||
|
pub symbolic: bool,
|
||||||
|
#[setters(skip)]
|
||||||
|
pub data: Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handle {
|
||||||
|
#[inline]
|
||||||
|
pub fn icon(self) -> Icon {
|
||||||
|
super::icon(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Data {
|
||||||
|
Name(Named),
|
||||||
|
Image(image::Handle),
|
||||||
|
Svg(svg::Handle),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an icon handle from its path.
|
||||||
|
pub fn from_path(path: PathBuf) -> Handle {
|
||||||
|
Handle {
|
||||||
|
symbolic: path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.is_some_and(|name| name.ends_with("-symbolic")),
|
||||||
|
data: if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext == OsStr::new("svg"))
|
||||||
|
{
|
||||||
|
Data::Svg(svg::Handle::from_path(path))
|
||||||
|
} else {
|
||||||
|
Data::Image(image::Handle::from_path(path))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an image handle from memory.
|
||||||
|
pub fn from_raster_bytes(
|
||||||
|
bytes: impl Into<Cow<'static, [u8]>>
|
||||||
|
+ std::convert::AsRef<[u8]>
|
||||||
|
+ std::marker::Send
|
||||||
|
+ std::marker::Sync
|
||||||
|
+ 'static,
|
||||||
|
) -> Handle {
|
||||||
|
fn inner(bytes: Cow<'static, [u8]>) -> Handle {
|
||||||
|
Handle {
|
||||||
|
symbolic: false,
|
||||||
|
data: match bytes {
|
||||||
|
Cow::Owned(b) => {
|
||||||
|
Data::Image(image::Handle::from_bytes(b))
|
||||||
|
}
|
||||||
|
Cow::Borrowed(b) => {
|
||||||
|
Data::Image(image::Handle::from_bytes(b))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner(bytes.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an image handle from RGBA data, where you must define the width and height.
|
||||||
|
pub fn from_raster_pixels(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
pixels: impl Into<Cow<'static, [u8]>>
|
||||||
|
+ std::convert::AsRef<[u8]>
|
||||||
|
+ std::marker::Send
|
||||||
|
+ std::marker::Sync,
|
||||||
|
) -> Handle {
|
||||||
|
fn inner(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
pixels: Cow<'static, [u8]>,
|
||||||
|
) -> Handle {
|
||||||
|
Handle {
|
||||||
|
symbolic: false,
|
||||||
|
data: match pixels {
|
||||||
|
Cow::Owned(pixels) => Data::Image(
|
||||||
|
image::Handle::from_rgba(width, height, pixels),
|
||||||
|
),
|
||||||
|
Cow::Borrowed(pixels) => Data::Image(
|
||||||
|
image::Handle::from_rgba(width, height, pixels),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner(width, height, pixels.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a SVG handle from memory.
|
||||||
|
pub fn from_svg_bytes(
|
||||||
|
bytes: impl Into<Cow<'static, [u8]>>,
|
||||||
|
) -> Handle {
|
||||||
|
Handle {
|
||||||
|
symbolic: false,
|
||||||
|
data: Data::Svg(svg::Handle::from_memory(bytes)),
|
||||||
|
}
|
||||||
|
}
|
||||||
187
src/ui/widgets/icon/mod.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
// Copyright 2022 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
//! Lazily-generated SVG icon widget for Iced.
|
||||||
|
|
||||||
|
mod named;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub use named::{IconFallback, Named};
|
||||||
|
|
||||||
|
mod handle;
|
||||||
|
pub use handle::{
|
||||||
|
from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes,
|
||||||
|
Data, Handle,
|
||||||
|
};
|
||||||
|
|
||||||
|
use derive_setters::Setters;
|
||||||
|
use iced::advanced::{image, svg};
|
||||||
|
use iced::widget::{Image, Svg};
|
||||||
|
use iced::Element;
|
||||||
|
use iced::Rotation;
|
||||||
|
use iced::{ContentFit, Length, Rectangle};
|
||||||
|
|
||||||
|
/// Create an [`Icon`] from a pre-existing [`Handle`]
|
||||||
|
pub fn icon(handle: Handle) -> Icon {
|
||||||
|
Icon {
|
||||||
|
content_fit: ContentFit::Fill,
|
||||||
|
handle,
|
||||||
|
height: None,
|
||||||
|
size: 16,
|
||||||
|
rotation: None,
|
||||||
|
width: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an icon handle from its XDG icon name.
|
||||||
|
pub fn from_name(name: impl Into<Arc<str>>) -> Named {
|
||||||
|
Named::new(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An image which may be an SVG or PNG.
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone, Setters)]
|
||||||
|
pub struct Icon {
|
||||||
|
#[setters(skip)]
|
||||||
|
handle: Handle,
|
||||||
|
pub(super) size: u16,
|
||||||
|
content_fit: ContentFit,
|
||||||
|
#[setters(strip_option)]
|
||||||
|
width: Option<Length>,
|
||||||
|
#[setters(strip_option)]
|
||||||
|
height: Option<Length>,
|
||||||
|
#[setters(strip_option)]
|
||||||
|
rotation: Option<Rotation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Icon {
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_svg_handle(
|
||||||
|
self,
|
||||||
|
) -> Option<iced::widget::svg::Handle> {
|
||||||
|
match self.handle.data {
|
||||||
|
Data::Name(named) => {
|
||||||
|
if let Some(path) = named.path() {
|
||||||
|
if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext == OsStr::new("svg"))
|
||||||
|
{
|
||||||
|
return Some(
|
||||||
|
iced::advanced::svg::Handle::from_path(
|
||||||
|
path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Data::Image(_) => (),
|
||||||
|
Data::Svg(handle) => return Some(handle),
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
|
||||||
|
let from_image = |handle| {
|
||||||
|
Image::new(handle)
|
||||||
|
.width(self.width.unwrap_or_else(|| {
|
||||||
|
Length::Fixed(f32::from(self.size))
|
||||||
|
}))
|
||||||
|
.height(self.height.unwrap_or_else(|| {
|
||||||
|
Length::Fixed(f32::from(self.size))
|
||||||
|
}))
|
||||||
|
.rotation(self.rotation.unwrap_or_default())
|
||||||
|
.content_fit(self.content_fit)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let from_svg = |handle| {
|
||||||
|
Svg::<crate::Theme>::new(handle)
|
||||||
|
.width(self.width.unwrap_or_else(|| {
|
||||||
|
Length::Fixed(f32::from(self.size))
|
||||||
|
}))
|
||||||
|
.height(self.height.unwrap_or_else(|| {
|
||||||
|
Length::Fixed(f32::from(self.size))
|
||||||
|
}))
|
||||||
|
.rotation(self.rotation.unwrap_or_default())
|
||||||
|
.content_fit(self.content_fit)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.handle.data {
|
||||||
|
Data::Name(named) => {
|
||||||
|
if let Some(path) = named.path() {
|
||||||
|
if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext == OsStr::new("svg"))
|
||||||
|
{
|
||||||
|
from_svg(svg::Handle::from_path(path))
|
||||||
|
} else {
|
||||||
|
from_image(image::Handle::from_path(path))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let bytes: &'static [u8] = &[];
|
||||||
|
from_svg(svg::Handle::from_memory(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Data::Image(handle) => from_image(handle),
|
||||||
|
Data::Svg(handle) => from_svg(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message: 'a> From<Icon> for Element<'a, Message> {
|
||||||
|
fn from(icon: Icon) -> Self {
|
||||||
|
icon.view::<Message>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw an icon in the given bounds via the runtime's renderer.
|
||||||
|
pub fn draw(
|
||||||
|
renderer: &mut iced::Renderer,
|
||||||
|
handle: &Handle,
|
||||||
|
icon_bounds: Rectangle,
|
||||||
|
) {
|
||||||
|
enum IcedHandle {
|
||||||
|
Svg(svg::Handle),
|
||||||
|
Image(image::Handle),
|
||||||
|
}
|
||||||
|
|
||||||
|
let iced_handle = match handle.clone().data {
|
||||||
|
Data::Name(named) => named.path().map(|path| {
|
||||||
|
if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext == OsStr::new("svg"))
|
||||||
|
{
|
||||||
|
IcedHandle::Svg(svg::Handle::from_path(path))
|
||||||
|
} else {
|
||||||
|
IcedHandle::Image(image::Handle::from_path(path))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Data::Image(handle) => Some(IcedHandle::Image(handle)),
|
||||||
|
Data::Svg(handle) => Some(IcedHandle::Svg(handle)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match iced_handle {
|
||||||
|
Some(IcedHandle::Svg(handle)) => svg::Renderer::draw_svg(
|
||||||
|
renderer,
|
||||||
|
svg::Svg::new(handle),
|
||||||
|
icon_bounds,
|
||||||
|
),
|
||||||
|
|
||||||
|
Some(IcedHandle::Image(handle)) => {
|
||||||
|
image::Renderer::draw_image(
|
||||||
|
renderer,
|
||||||
|
(&handle).into(),
|
||||||
|
icon_bounds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/ui/widgets/icon/named.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
// Copyright 2023 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use super::{Handle, Icon};
|
||||||
|
use std::{borrow::Cow, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Hash)]
|
||||||
|
/// Fallback icon to use if the icon was not found.
|
||||||
|
pub enum IconFallback {
|
||||||
|
#[default]
|
||||||
|
/// Default fallback using the icon name.
|
||||||
|
Default,
|
||||||
|
/// Fallback to specific icon names.
|
||||||
|
Names(Vec<Cow<'static, str>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(derive_setters::Setters, Clone, Debug, Hash)]
|
||||||
|
pub struct Named {
|
||||||
|
/// Name of icon to locate in an XDG icon path.
|
||||||
|
pub(super) name: Arc<str>,
|
||||||
|
|
||||||
|
/// Checks for a fallback if the icon was not found.
|
||||||
|
pub fallback: Option<IconFallback>,
|
||||||
|
|
||||||
|
/// Restrict the lookup to a given scale.
|
||||||
|
#[setters(strip_option)]
|
||||||
|
pub scale: Option<u16>,
|
||||||
|
|
||||||
|
/// Restrict the lookup to a given size.
|
||||||
|
#[setters(strip_option)]
|
||||||
|
pub size: Option<u16>,
|
||||||
|
|
||||||
|
/// Whether the icon is symbolic or not.
|
||||||
|
pub symbolic: bool,
|
||||||
|
|
||||||
|
/// Prioritizes SVG over PNG
|
||||||
|
pub prefer_svg: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Named {
|
||||||
|
pub fn new(name: impl Into<Arc<str>>) -> Self {
|
||||||
|
let name = name.into();
|
||||||
|
Self {
|
||||||
|
symbolic: name.ends_with("-symbolic"),
|
||||||
|
name,
|
||||||
|
fallback: Some(IconFallback::Default),
|
||||||
|
size: None,
|
||||||
|
scale: None,
|
||||||
|
prefer_svg: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
#[must_use]
|
||||||
|
pub fn path(self) -> Option<PathBuf> {
|
||||||
|
let name = &*self.name;
|
||||||
|
let fallback = &self.fallback;
|
||||||
|
let locate = |theme: &str, name| {
|
||||||
|
let mut lookup = freedesktop_icons::lookup(name)
|
||||||
|
.with_theme(theme.as_ref())
|
||||||
|
.with_cache();
|
||||||
|
|
||||||
|
if let Some(scale) = self.scale {
|
||||||
|
lookup = lookup.with_scale(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(size) = self.size {
|
||||||
|
lookup = lookup.with_size(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.prefer_svg {
|
||||||
|
lookup = lookup.force_svg();
|
||||||
|
}
|
||||||
|
lookup.find()
|
||||||
|
};
|
||||||
|
|
||||||
|
let theme = "Papirus-Dark";
|
||||||
|
let themes = if theme.as_ref() == "Cosmic" {
|
||||||
|
vec![theme.as_ref()]
|
||||||
|
} else {
|
||||||
|
vec![theme.as_ref(), "Cosmic"]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = themes.iter().find_map(|t| locate(t, name));
|
||||||
|
|
||||||
|
// On failure, attempt to locate fallback icon.
|
||||||
|
if result.is_none() {
|
||||||
|
if matches!(fallback, Some(IconFallback::Default)) {
|
||||||
|
for new_name in name
|
||||||
|
.rmatch_indices('-')
|
||||||
|
.map(|(pos, _)| &name[..pos])
|
||||||
|
{
|
||||||
|
result = themes
|
||||||
|
.iter()
|
||||||
|
.find_map(|t| locate(t, new_name));
|
||||||
|
if result.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(IconFallback::Names(fallbacks)) =
|
||||||
|
fallback
|
||||||
|
{
|
||||||
|
for fallback in fallbacks {
|
||||||
|
result = themes
|
||||||
|
.iter()
|
||||||
|
.find_map(|t| locate(t, fallback));
|
||||||
|
if result.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn path(self) -> Option<PathBuf> {
|
||||||
|
//TODO: implement icon lookup for Windows
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn handle(self) -> Handle {
|
||||||
|
Handle {
|
||||||
|
symbolic: self.symbolic,
|
||||||
|
data: super::Data::Name(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn icon(self) -> Icon {
|
||||||
|
let size = self.size;
|
||||||
|
|
||||||
|
let icon = super::icon(self.handle());
|
||||||
|
|
||||||
|
match size {
|
||||||
|
Some(size) => icon.size(size),
|
||||||
|
None => icon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Named> for Handle {
|
||||||
|
#[inline]
|
||||||
|
fn from(builder: Named) -> Self {
|
||||||
|
builder.handle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Named> for Icon {
|
||||||
|
#[inline]
|
||||||
|
fn from(builder: Named) -> Self {
|
||||||
|
builder.icon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message: 'static> From<Named> for crate::Element<'_, Message> {
|
||||||
|
#[inline]
|
||||||
|
fn from(builder: Named) -> Self {
|
||||||
|
builder.icon().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,2 @@
|
||||||
#[allow(clippy::unwrap_used)]
|
// pub mod slide_text;
|
||||||
#[allow(clippy::nursery)]
|
pub mod icon;
|
||||||
#[allow(clippy::pedantic)]
|
|
||||||
pub mod draggable;
|
|
||||||
pub mod slide_text;
|
|
||||||
pub mod verse_editor;
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,69 @@
|
||||||
use cosmic::iced::advanced::layout::{self, Layout};
|
use femtovg::renderer::WGPURenderer;
|
||||||
use cosmic::iced::advanced::renderer;
|
use femtovg::{Canvas, TextContext};
|
||||||
use cosmic::iced::advanced::widget::{self, Widget};
|
use iced::iced::advanced::layout::{self, Layout};
|
||||||
use cosmic::iced::border;
|
use iced::iced::advanced::renderer;
|
||||||
use cosmic::iced::mouse;
|
use iced::iced::advanced::widget::{self, Widget};
|
||||||
use cosmic::iced::{Color, Element, Length, Rectangle, Size};
|
use iced::iced::border;
|
||||||
use cosmic::iced_wgpu::Primitive;
|
use iced::iced::mouse;
|
||||||
use cosmic::iced_wgpu::primitive::Renderer as PrimitiveRenderer;
|
use iced::iced::{Color, Element, Length, Rectangle, Size};
|
||||||
|
|
||||||
pub struct SlideText {
|
pub struct SlideText {
|
||||||
_text: String,
|
text: String,
|
||||||
font_size: f32,
|
font_size: f32,
|
||||||
|
canvas: Canvas<WGPURenderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlideText {
|
impl SlideText {
|
||||||
pub fn new(text: impl AsRef<str>) -> Self {
|
pub async fn new(text: &str) -> Self {
|
||||||
let text = text.as_ref();
|
let backends = wgpu::Backends::PRIMARY;
|
||||||
|
let instance =
|
||||||
|
wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||||
|
backends,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let surface =
|
||||||
|
instance.create_surface(window.clone()).unwrap();
|
||||||
|
let adapter = iced::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
|
||||||
|
.await
|
||||||
|
.expect("Failed to find an appropriate adapter");
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(
|
||||||
|
&wgpu::DeviceDescriptor {
|
||||||
|
label: None,
|
||||||
|
required_features: adapter.features(),
|
||||||
|
required_limits: wgpu::Limits::default(),
|
||||||
|
memory_hints: wgpu::MemoryHints::Performance,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to device it");
|
||||||
|
let renderer = WGPURenderer::new(device, queue);
|
||||||
|
let canvas =
|
||||||
|
Canvas::new_with_text_context(renderer, text_context)
|
||||||
|
.expect("oops femtovg");
|
||||||
Self {
|
Self {
|
||||||
_text: text.to_string(),
|
text: text.to_owned(),
|
||||||
font_size: 50.0,
|
font_size: 50.0,
|
||||||
|
canvas,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn slide_text(text: impl AsRef<str>) -> SlideText {
|
fn get_canvas(text_context: TextContext) -> Canvas {
|
||||||
|
let renderer = WGPURenderer::new(device, queue);
|
||||||
|
Canvas::new_with_text_context(renderer, text_context)
|
||||||
|
.expect("oops femtovg")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slide_text(text: &str) -> SlideText {
|
||||||
SlideText::new(text)
|
SlideText::new(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
for SlideText
|
for SlideText
|
||||||
where
|
where
|
||||||
Message: Clone,
|
Renderer: renderer::Renderer,
|
||||||
Renderer: PrimitiveRenderer,
|
|
||||||
{
|
{
|
||||||
fn size(&self) -> Size<Length> {
|
fn size(&self) -> Size<Length> {
|
||||||
Size {
|
Size {
|
||||||
|
|
@ -61,8 +94,6 @@ where
|
||||||
_cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
renderer
|
|
||||||
.draw_primitive(layout.bounds(), TextPrimitive::new());
|
|
||||||
renderer.fill_quad(
|
renderer.fill_quad(
|
||||||
renderer::Quad {
|
renderer::Quad {
|
||||||
bounds: layout.bounds(),
|
bounds: layout.bounds(),
|
||||||
|
|
@ -74,50 +105,12 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer> From<SlideText>
|
impl<Message, Theme, Renderer> From<SlideText>
|
||||||
for Element<'a, Message, Theme, Renderer>
|
for Element<'_, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Message: 'a + Clone,
|
Renderer: renderer::Renderer,
|
||||||
Theme: 'a,
|
|
||||||
Renderer: 'a + PrimitiveRenderer,
|
|
||||||
{
|
{
|
||||||
fn from(slide_text: SlideText) -> Self {
|
fn from(circle: SlideText) -> Self {
|
||||||
Self::new(slide_text)
|
Self::new(circle)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(crate) struct TextPrimitive {
|
|
||||||
_text_id: u64,
|
|
||||||
_size: (u32, u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextPrimitive {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Primitive for TextPrimitive {
|
|
||||||
fn prepare(
|
|
||||||
&self,
|
|
||||||
_device: &cosmic::iced::wgpu::Device,
|
|
||||||
_queue: &cosmic::iced::wgpu::Queue,
|
|
||||||
_format: cosmic::iced::wgpu::TextureFormat,
|
|
||||||
_storage: &mut cosmic::iced_widget::shader::Storage,
|
|
||||||
_bounds: &Rectangle,
|
|
||||||
_viewport: &cosmic::iced_wgpu::graphics::Viewport,
|
|
||||||
) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(
|
|
||||||
&self,
|
|
||||||
_encoder: &mut cosmic::iced::wgpu::CommandEncoder,
|
|
||||||
_storage: &cosmic::iced_widget::shader::Storage,
|
|
||||||
_target: &cosmic::iced::wgpu::TextureView,
|
|
||||||
_clip_bounds: &Rectangle<u32>,
|
|
||||||
) {
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
use cosmic::{
|
|
||||||
Element, Task,
|
|
||||||
cosmic_theme::palette::WithAlpha,
|
|
||||||
iced::{Background, Border},
|
|
||||||
iced_widget::{column, row},
|
|
||||||
theme,
|
|
||||||
widget::{
|
|
||||||
button, combo_box, container, horizontal_space, icon,
|
|
||||||
text_editor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::core::songs::VerseName;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct VerseEditor {
|
|
||||||
pub verse_name: VerseName,
|
|
||||||
pub lyric: String,
|
|
||||||
content: text_editor::Content,
|
|
||||||
editing_verse_name: bool,
|
|
||||||
verse_name_combo: combo_box::State<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Message {
|
|
||||||
UpdateLyric(text_editor::Action),
|
|
||||||
UpdateVerseName(String),
|
|
||||||
EditVerseName,
|
|
||||||
DeleteVerse(VerseName),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Action {
|
|
||||||
Task(Task<Message>),
|
|
||||||
UpdateVerse((VerseName, String)),
|
|
||||||
UpdateVerseName(String),
|
|
||||||
DeleteVerse(VerseName),
|
|
||||||
ScrollVerses(f32),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VerseEditor {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(verse: VerseName, lyric: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
verse_name: verse,
|
|
||||||
lyric: lyric.to_string(),
|
|
||||||
content: text_editor::Content::with_text(lyric),
|
|
||||||
editing_verse_name: false,
|
|
||||||
verse_name_combo: combo_box::State::new(
|
|
||||||
VerseName::all_names(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.content.perform(action);
|
|
||||||
Action::None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::UpdateVerseName(verse_name) => {
|
|
||||||
Action::UpdateVerseName(verse_name)
|
|
||||||
}
|
|
||||||
Message::EditVerseName => {
|
|
||||||
self.editing_verse_name = !self.editing_verse_name;
|
|
||||||
Action::None
|
|
||||||
}
|
|
||||||
Message::DeleteVerse(verse) => Action::DeleteVerse(verse),
|
|
||||||
Message::None => Action::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view(&self) -> Element<Message> {
|
|
||||||
let cosmic::cosmic_theme::Spacing {
|
|
||||||
space_xxs: _,
|
|
||||||
space_s,
|
|
||||||
space_m,
|
|
||||||
..
|
|
||||||
} = theme::spacing();
|
|
||||||
|
|
||||||
let delete_button = button::text("Delete")
|
|
||||||
.trailing_icon(
|
|
||||||
icon::from_name("view-close").symbolic(true),
|
|
||||||
)
|
|
||||||
.class(theme::Button::Destructive)
|
|
||||||
.on_press(Message::DeleteVerse(self.verse_name));
|
|
||||||
let combo = combo_box(
|
|
||||||
&self.verse_name_combo,
|
|
||||||
"Verse 1",
|
|
||||||
Some(&self.verse_name.get_name()),
|
|
||||||
Message::UpdateVerseName,
|
|
||||||
);
|
|
||||||
|
|
||||||
let verse_title =
|
|
||||||
row![combo, horizontal_space(), delete_button];
|
|
||||||
|
|
||||||
let lyric: Element<Message> = if self.verse_name
|
|
||||||
== VerseName::Blank
|
|
||||||
{
|
|
||||||
horizontal_space().into()
|
|
||||||
} else {
|
|
||||||
text_editor(&self.content)
|
|
||||||
.on_action(Message::UpdateLyric)
|
|
||||||
.padding(space_m)
|
|
||||||
.class(theme::iced::TextEditor::Custom(Box::new(
|
|
||||||
move |t, s| {
|
|
||||||
let neutral = t.cosmic().palette.neutral_9;
|
|
||||||
let mut base_style = text_editor::Style {
|
|
||||||
background: Background::Color(
|
|
||||||
t.cosmic()
|
|
||||||
.background
|
|
||||||
.small_widget
|
|
||||||
.with_alpha(0.25)
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
border: Border::default()
|
|
||||||
.rounded(space_s)
|
|
||||||
.width(2)
|
|
||||||
.color(
|
|
||||||
t.cosmic().bg_component_divider(),
|
|
||||||
),
|
|
||||||
icon: t
|
|
||||||
.cosmic()
|
|
||||||
.primary_component_color()
|
|
||||||
.into(),
|
|
||||||
placeholder: neutral
|
|
||||||
.with_alpha(0.7)
|
|
||||||
.into(),
|
|
||||||
value: neutral.into(),
|
|
||||||
selection: t.cosmic().accent.base.into(),
|
|
||||||
};
|
|
||||||
let hovered_border = Border::default()
|
|
||||||
.rounded(space_s)
|
|
||||||
.width(3)
|
|
||||||
.color(t.cosmic().accent.hover);
|
|
||||||
match s {
|
|
||||||
text_editor::Status::Active => base_style,
|
|
||||||
text_editor::Status::Hovered
|
|
||||||
| text_editor::Status::Focused => {
|
|
||||||
base_style.border = hovered_border;
|
|
||||||
base_style
|
|
||||||
}
|
|
||||||
text_editor::Status::Disabled => {
|
|
||||||
base_style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
.height(150)
|
|
||||||
.into()
|
|
||||||
};
|
|
||||||
|
|
||||||
container(column![verse_title, lyric].spacing(space_s))
|
|
||||||
.padding(space_s)
|
|
||||||
.class(theme::Container::Card)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
test.db
|
|
@ -1,11 +1,10 @@
|
||||||
(slide :background (image :source "~/pics/frodo.jpg" :fit fill)
|
(slide :background (image :source "~/pics/frodo.jpg" :fit fill)
|
||||||
(text "This is frodo" :font-size 140))
|
(text "This is frodo" :font-size 90))
|
||||||
(slide (video :source "~/vids/test/camprules2024.mp4" :fit contain))
|
(slide (video :source "~/vids/test/camprules2024.mp4" :fit contain))
|
||||||
;; (slide (video :source "~/vids/never give up.mkv" :fit contain))
|
(slide (video :source "~/vids/never give up.mkv" :fit contain))
|
||||||
;; (slide (video :source "~/vids/The promise of Rust.mkv" :fit contain))
|
(slide (video :source "~/vids/The promise of Rust.mkv" :fit contain))
|
||||||
(slide :background (presentation :source "~/docs/description-of-a-discipled-person-assessment-2016.pdf" :fit contain))
|
|
||||||
(song :id 7 :author "North Point Worship"
|
(song :id 7 :author "North Point Worship"
|
||||||
:font "Quicksand" :font-size 120
|
:font "Quicksand Bold" :font-size 60
|
||||||
:shadow "" :stroke ""
|
:shadow "" :stroke ""
|
||||||
:title "Death Was Arrested"
|
:title "Death Was Arrested"
|
||||||
:background (image :source "file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
|
:background (image :source "file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,12 @@
|
||||||
kind: Image
|
kind: Image
|
||||||
),
|
),
|
||||||
text: "This is Frodo",
|
text: "This is Frodo",
|
||||||
font: Some(Font(
|
font: "Quicksand",
|
||||||
name: "Quicksand",
|
font_size: 50,
|
||||||
weight: Normal,
|
|
||||||
style: Normal,
|
|
||||||
size: 130,
|
|
||||||
)),
|
|
||||||
font_size: 130,
|
|
||||||
stroke: None,
|
|
||||||
shadow: None,
|
|
||||||
text_alignment: MiddleCenter,
|
text_alignment: MiddleCenter,
|
||||||
video_loop: false,
|
video_loop: false,
|
||||||
video_start_time: 0.0,
|
video_start_time: 0.0,
|
||||||
video_end_time: 0.0,
|
video_end_time: 0.0,
|
||||||
pdf_index: 0,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
(
|
(
|
||||||
|
|
@ -29,19 +21,11 @@
|
||||||
kind: Video
|
kind: Video
|
||||||
),
|
),
|
||||||
text: "This is Frodo",
|
text: "This is Frodo",
|
||||||
font: Some(Font(
|
font: "Quicksand",
|
||||||
name: "Quicksand",
|
font_size: 50,
|
||||||
weight: Normal,
|
|
||||||
style: Normal,
|
|
||||||
size: 130,
|
|
||||||
)),
|
|
||||||
font_size: 130,
|
|
||||||
stroke: None,
|
|
||||||
shadow: None,
|
|
||||||
text_alignment: MiddleCenter,
|
text_alignment: MiddleCenter,
|
||||||
video_loop: false,
|
video_loop: false,
|
||||||
video_start_time: 0.0,
|
video_start_time: 0.0,
|
||||||
video_end_time: 0.0,
|
video_end_time: 0.0,
|
||||||
pdf_index: 0,
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
(song :id 7 :author "North Point Worship"
|
(song :id 7 :author "North Point Worship"
|
||||||
:font "Quicksand" :font-size 140
|
:font "Quicksand Bold" :font-size 60
|
||||||
:title "Death Was Arrested"
|
:title "Death Was Arrested"
|
||||||
:background (image :source "~/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
|
:background (image :source "~/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
|
||||||
:text-alignment center
|
:text-alignment center
|
||||||
|
|
|
||||||
63
todo.org
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#+TITLE: The Task list for Lumina
|
||||||
|
|
||||||
|
|
||||||
|
* TODO [#A] Develop DnD for library items
|
||||||
|
This is limited by the fact that I need to develop this in cosmic. I am honestly thinking that I'll need to build my own drag and drop system or at least work with system76 to fix their dnd system on other systems.
|
||||||
|
|
||||||
|
This needs lots more attention
|
||||||
|
|
||||||
|
* TODO [#A] Need to fix tests now that the basic app is working
|
||||||
|
|
||||||
|
* TODO Check into =mupdf-rs= for loading PDF's.
|
||||||
|
* TODO [#A] 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.
|
||||||
|
|
||||||
|
Actually, what if we just made the svg at load/creation time and stored it in the file system for later, then load the entire songs svg's into memory during the presentation to speed things up? Would that be faster than creating them at on the fly? Is it the creation of them that is slow or the rendering?
|
||||||
|
|
||||||
|
** SVG performs badly
|
||||||
|
Since SVG's apparently run poorly in iced, instead I'll need to see about either creating a new text element, or teaching Iced to render strokes and shadows on text.
|
||||||
|
|
||||||
|
* TODO [#C] Make the presenter more modular so things are easier to change.
|
||||||
|
|
||||||
|
* TODO Build library to see all available songs, images, videos, presentations, and slides
|
||||||
|
** DONE Develop ui for libraries
|
||||||
|
I've got the library basic layer done, I need to develop a way to open the libraries accordion button and then show the list of items in the library
|
||||||
|
* TODO [#B] Build editors for each possible item
|
||||||
|
** TODO Develop ui for editors
|
||||||
|
|
||||||
|
* TODO [#B] Develop ui for settings
|
||||||
|
|
||||||
|
* TODO [#B] Develop library system for slides that are more than images or video i.e. content
|
||||||
|
|
||||||
|
* TODO [#B] Functions for text alignments
|
||||||
|
This will need to be matched on for the =TextAlignment= from the user
|
||||||
|
* TODO [#C] Figure out why the Video element seems to have problems when moving the mouse around
|
||||||
|
* TODO [#B] Find a way to load and discover every font on the system for slide building
|
||||||
|
This may not be necessary since it is possible to create a font using =Box::leak()=.
|
||||||
|
#+begin_src rust
|
||||||
|
let font = self.current_slide.font().into_boxed_str();
|
||||||
|
let family = Family::Name(Box::leak(font));
|
||||||
|
let weight = Weight::Normal;
|
||||||
|
let stretch = Stretch::Normal;
|
||||||
|
let style = Style::Normal;
|
||||||
|
let font = Font {
|
||||||
|
family,
|
||||||
|
weight,
|
||||||
|
stretch,
|
||||||
|
style,
|
||||||
|
};
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
This code creates a font by leaking the Box to a ='static &str=. I just am not sure if the &str stays around in memory after the view function. If it does, then it's not on the stack anymore and should be fine, but if it isn't cleaned up then we will have a memory leak.
|
||||||
|
|
||||||
|
Krimzin on Discord told me that maybe the =update= method is a better place for this Box to be created or updated and then maybe I could generate the view from there.
|
||||||
|
|
||||||
|
* DONE Use Rich Text instead of normal text for slides
|
||||||
|
This will make it so that we can add styling to the text like borders and backgrounds or highlights. Maybe in the future it'll add shadows too.
|
||||||
|
* DONE Find a way for text to pass through a service item to a slide i.e. content piece
|
||||||
|
This proved easier by just creating the =Slide= first and inserting it into the =ServiceItem=.
|
||||||
|
* DONE [#A] Change return type of all components to an Action enum instead of the Task<Message> type [0%] [0/0]
|
||||||
|
** DONE Library
|
||||||
|
** DONE SongEditor
|
||||||
|
** DONE Presenter
|
||||||
|
|
||||||