Compare commits

..

4 commits

Author SHA1 Message Date
80a9b48ae9 my fork of iced_video_player
Some checks failed
/ test (push) Has been cancelled
2025-08-27 09:02:46 -05:00
1861f357a8 well crap
Some checks are pending
/ test (push) Waiting to run
2025-08-26 15:25:04 -05:00
4ae6a9a9a7 ohh
Some checks are pending
/ test (push) Waiting to run
2025-08-26 09:47:35 -05:00
c886d18134 beginning the switch... lots to go yet
Some checks are pending
/ test (push) Waiting to run
2025-08-26 09:44:06 -05:00
53 changed files with 4068 additions and 11385 deletions

5
.gitignore vendored
View file

@ -3,8 +3,3 @@
.sqlx
.env
data.db
/flamegraph.svg
/.zed/
/perf.data
/perf.data.old
.aider*

4127
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,9 +8,12 @@ description = "A cli presentation system"
[dependencies]
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"] }
pretty_assertions = "1.4.1"
serde = { version = "1.0.213", features = ["derive"] }
serde-lexpr = "0.1.3"
tracing = "0.1.40"
tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] }
@ -18,10 +21,10 @@ strum = "0.26.3"
strum_macros = "0.26.4"
ron = "0.8.1"
sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio"] }
dirs = "6.0.0"
dirs = "5.0.1"
tokio = "1.41.1"
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-app = "0.23"
# gstreamer-video = "0.23"
@ -30,30 +33,23 @@ gstreamer-app = "0.23"
url = "2"
colors-transform = "0.2.11"
rayon = "1.11.0"
resvg = "0.45.1"
image = "0.25.8"
rapidhash = "4.0.0"
rapidfuzz = "0.5.0"
# dragking = { git = "https://github.com/airstrike/dragking" }
# resvg = "0.45.1"
# femtovg = { version = "0.16.0", features = ["wgpu"] }
# wgpu = "26.0.1"
# mupdf = "0.5.0"
mupdf = { version = "0.5.0", git = "https://github.com/messense/mupdf-rs", rev="2425c1405b326165b06834dcc1ca859015f92787"}
tar = "0.4.44"
zstd = "0.13.3"
fastrand = "2.3.0"
rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false }
derive_setters = "0.1.8"
freedesktop-icons = "0.4.0"
# rfd = { version = "0.15.4", default-features = false, features = ["xdg-portal"] }
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
default-features = false
features = ["debug", "winit", "desktop", "winit_wgpu", "winit_tokio", "tokio", "wayland", "rfd", "dbus-config", "a11y", "wgpu", "multi-window", "process"]
[dependencies.iced]
git = "https://github.com/iced-rs/iced"
branch = "master"
features = ["wgpu", "image", "advanced", "svg", "canvas", "hot", "debug", "lazy", "tokio"]
[dependencies.iced_video_player]
git = "https://github.com/jackpot51/iced_video_player.git"
branch = "cosmic"
features = ["wgpu"]
git = "https://git.tfcconnection.org/chris/iced_video_player"
branch = "master"
# branch = "cosmic"
# [profile.dev]
# opt-level = 3

24
flake.lock generated
View file

@ -6,11 +6,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1759214609,
"narHash": "sha256-+V3SeMjAMd9j9JTECk9oc0gWhtsk79rFEbYf/tHjywo=",
"lastModified": 1755585599,
"narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=",
"owner": "nix-community",
"repo": "fenix",
"rev": "f93a2d7225bc7a93d3379acff8fe722e21d97852",
"rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42",
"type": "github"
},
"original": {
@ -80,11 +80,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"lastModified": 1755186698,
"narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c",
"type": "github"
},
"original": {
@ -112,11 +112,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"lastModified": 1755615617,
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
"type": "github"
},
"original": {
@ -137,11 +137,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1759134797,
"narHash": "sha256-YPi+jL3tx/yC5J5l7/OB7Lnlr9BMTzYnZtm7tRJzUNg=",
"lastModified": 1755504847,
"narHash": "sha256-VX0B9hwhJypCGqncVVLC+SmeMVd/GAYbJZ0MiiUn2Pk=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "062ac7a5451e8e92a32e22a60d86882d6a034f3f",
"rev": "a905e3b21b144d77e1b304e49f3264f6f8d4db75",
"type": "github"
},
"original": {

View file

@ -8,18 +8,16 @@
fenix.url = "github:nix-community/fenix";
};
outputs =
inputs:
with inputs;
flake-utils.lib.eachDefaultSystem (
system:
outputs = inputs: with inputs;
flake-utils.lib.eachDefaultSystem
(system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ fenix.overlays.default ];
overlays = [fenix.overlays.default];
# overlays = [cargo2nix.overlays.default];
};
naersk' = pkgs.callPackage naersk { };
naersk' = pkgs.callPackage naersk {};
nbi = with pkgs; [
# Rust tools
alejandra
@ -46,15 +44,12 @@
gdb
lldb
cmake
clang
libclang
makeWrapper
vulkan-headers
vulkan-loader
vulkan-tools
libGL
cargo-flamegraph
bacon
fontconfig
glib
@ -67,47 +62,31 @@
gst_all_1.gst-plugins-rs
gst_all_1.gst-vaapi
gst_all_1.gstreamer
# podofo
# mpv
ffmpeg-full
mupdf
# yt-dlp
just
sqlx-cli
cargo-watch
];
in
rec {
devShell =
pkgs.mkShell.override
in rec
{
devShell = pkgs.mkShell.override {
# stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.clangStdenv;
}
{
} {
nativeBuildInputs = nbi;
buildInputs = bi;
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${
with pkgs;
pkgs.lib.makeLibraryPath [
pkgs.alsa-lib
pkgs.gst_all_1.gst-libav
pkgs.gst_all_1.gstreamer
pkgs.gst_all_1.gst-plugins-bad
pkgs.gst_all_1.gst-plugins-good
pkgs.gst_all_1.gst-plugins-ugly
pkgs.gst_all_1.gst-plugins-base
pkgs.gst_all_1.gst-plugins-rs
pkgs.gst_all_1.gst-vaapi
pkgs.glib
pkgs.fontconfig
pkgs.vulkan-loader
pkgs.wayland
pkgs.wayland-protocols
pkgs.libxkbcommon
pkgs.mupdf
pkgs.libclang
]
}";
# LIBCLANG_PATH = "${pkgs.clang}";
DATABASE_URL = "sqlite:///home/chris/.local/share/lumina/library-db.sqlite3";
};
defaultPackage = naersk'.buildPackage {

View file

@ -8,8 +8,6 @@ build:
sbuild:
RUST_LOG=debug sccache cargo build
run:
RUST_LOG=debug cargo run -- {{ui}}
run-file:
RUST_LOG=debug cargo run -- {{ui}} {{file}}
srun:
RUST_LOG=debug sccache cargo run -- {{ui}} {{file}}
@ -22,6 +20,5 @@ profile:
alias b := build
alias r := run
alias rf := run-file
alias sr := srun
alias c := clean

View file

@ -1,3 +0,0 @@
-- Add migration script here
ALTER TABLE presentations
DROP COLUMN pageCount;

View file

@ -1,6 +0,0 @@
-- Add migration script here
ALTER TABLE presentations
ADD COLUMN starting_index INTEGER;
ALTER TABLE presentations
ADD COLUMN ending_index INTEGER;

View file

@ -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.
* 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.
** Features (planned are in parentheses)
- Presents songs lyrics with image and video backgrounds
- 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)
- (Custom slide builder)
- (an intuitive UI) - Still needs A LOT of polish

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 KiB

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,3 +1,3 @@
max_width = 70
style_edition = "2024"
# style_edition = "2018"
# version = "Two"

View file

@ -1,408 +1,356 @@
use crate::core::{
kinds::ServiceItemKind, service_items::ServiceItem,
slide::Background,
};
use miette::{IntoDiagnostic, Result};
use std::{
fs::{self, File},
io::Write,
iter,
path::{Path, PathBuf},
};
use tar::Builder;
use tar::{Archive, Builder};
use tracing::error;
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}};
pub async fn save(
list: Vec<ServiceItem>,
path: impl AsRef<Path>,
) -> Result<()> {
pub async fn save(list: Vec<ServiceItem>, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
let save_file = File::create(path).into_diagnostic()?;
let ron = process_service_items(&list).await?;
let save_file = File::create(path)?;
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(())
}
async fn store_service_items(items: &Vec<ServiceItem>, db: &mut SqliteConnection, save_file: &File, json: &Value) -> Result<()> {
let encoder = Encoder::new(save_file, 3).unwrap();
let mut tar = Builder::new(encoder);
let mut temp_dir = dirs::data_dir().unwrap();
temp_dir.push("lumina");
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_");
temp_dir.push(s);
fs::create_dir_all(&temp_dir).into_diagnostic()?;
let service_file = temp_dir.join("serviceitems.ron");
fs::File::create(&service_file).into_diagnostic()?;
match fs::File::options()
.read(true)
.write(true)
.open(service_file)
{
Ok(mut f) => {
f.write(ron.as_bytes()).into_diagnostic()?;
}
Err(e) => {
error!("There were problems making a file i guess: {e}")
}
fs::create_dir_all(&temp_dir)?;
let service_file = temp_dir.join("serviceitems.json");
fs::File::create(&service_file)?;
match fs::File::options().read(true).write(true).open(service_file) {
Ok(f) => {
serde_json::to_writer_pretty(f, json)?;
},
Err(e) => error!("There were problems making a file i guess: {e}"),
};
// let list list.iter_mut().map(|item| {
// match item.kind {
// ServiceItemKind::Song(mut song) => {
// song.background
// }
// }
// }).collect();
for item in list {
for item in items {
let background;
let audio: Option<PathBuf>;
match &item.kind {
ServiceItemKind::Song(song) => {
background = song.background.clone();
audio = song.audio.clone();
}
ServiceItemKind::Image(image) => {
background = Some(
Background::try_from(image.path.clone())
.into_diagnostic()?,
);
match item.kind {
ServiceItemKind::Song => {
let song = get_song_from_db(item.database_id, db).await?;
background = song.background;
audio = song.audio;
},
ServiceItemKind::Image => {
let image = get_image_from_db(item.database_id, db).await?;
background = Some(Background::try_from(image.path)?);
audio = None;
}
ServiceItemKind::Video(video) => {
background = Some(
Background::try_from(video.path.clone())
.into_diagnostic()?,
);
},
ServiceItemKind::Video => {
let video = get_video_from_db(item.database_id, db).await?;
background = Some(Background::try_from(video.path)?);
audio = None;
}
ServiceItemKind::Presentation(presentation) => {
background = Some(
Background::try_from(presentation.path.clone())
.into_diagnostic()?,
);
},
ServiceItemKind::Presentation(_) => {
let presentation = get_presentation_from_db(item.database_id, db).await?;
background = Some(Background::try_from(presentation.path)?);
audio = None;
}
ServiceItemKind::Content(_slide) => {
},
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",
));
let audio_file = temp_dir.join(file.file_name().expect("Audio file couldn't be added to temp_dir"));
if let Ok(file) = file.strip_prefix("file://") {
fs::File::create(&audio_file).into_diagnostic()?;
fs::copy(file, audio_file).into_diagnostic()?;
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).into_diagnostic()?;
fs::copy(file, audio_file).into_diagnostic()?;
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");
}
};
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",
));
let background_file = temp_dir.join(file.path.file_name().expect("Background file couldn't be added to temp_dir"));
if let Ok(file) = file.path.strip_prefix("file://") {
fs::File::create(&background_file)
.into_diagnostic()?;
fs::copy(file, background_file).into_diagnostic()?;
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)
.into_diagnostic()?;
fs::copy(file.path, background_file)
.into_diagnostic()?;
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");
}
}
}
tar.append_dir_all(path, temp_dir).into_diagnostic()?;
tar.finish().into_diagnostic()
Ok(())
}
async fn clear_temp_dir(temp_dir: &Path) -> Result<()> {
todo!()
}
async fn process_service_items(
items: &Vec<ServiceItem>,
) -> Result<String> {
Ok(items
.into_iter()
.filter_map(|item| {
let ron = ron::ser::to_string(item);
ron.ok()
})
.collect())
async fn process_service_items(items: &Vec<ServiceItem>, db: &mut SqliteConnection) -> Result<Value> {
let mut values: Vec<Value> = vec![];
for item in items {
match item.kind {
ServiceItemKind::Song => {
let value = process_song(item.database_id, db).await?;
values.push(value);
},
ServiceItemKind::Image => {
let value = process_image(item.database_id, db).await?;
values.push(value);
},
ServiceItemKind::Video => {
let value = process_video(item.database_id, db).await?;
values.push(value);
},
ServiceItemKind::Presentation(_) => {
let value = process_presentation(item.database_id, db).await?;
values.push(value);
},
ServiceItemKind::Content => {
todo!()
},
}
}
let json = Value::from(values);
Ok(json)
}
// async fn process_song(
// database_id: i32,
// db: &mut SqliteConnection,
// ) -> Result<Value> {
// let song = get_song_from_db(database_id, db).await?;
// let song_ron = ron::to_value(&song)?;
// let kind_ron = ron::to_value(ServiceItemKind::Song)?;
// let json =
// serde_json::json!({"item": song_json, "kind": kind_json});
// Ok(json)
// }
async fn process_song(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
let song = get_song_from_db(database_id, db).await?;
let song_json = serde_json::to_value(&song)?;
let kind_json = serde_json::to_value(ServiceItemKind::Song)?;
let json = serde_json::json!({"item": song_json, "kind": kind_json});
Ok(json)
}
// async fn process_image(
// database_id: i32,
// db: &mut SqliteConnection,
// ) -> Result<Value> {
// let image = get_image_from_db(database_id, db).await?;
// let image_json = serde_json::to_value(&image)?;
// let kind_json = serde_json::to_value(ServiceItemKind::Image)?;
// let json =
// serde_json::json!({"item": image_json, "kind": kind_json});
// Ok(json)
// }
async fn process_image(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
let image = get_image_from_db(database_id, db).await?;
let image_json = serde_json::to_value(&image)?;
let kind_json = serde_json::to_value(ServiceItemKind::Image)?;
let json = serde_json::json!({"item": image_json, "kind": kind_json});
Ok(json)
}
// async fn process_video(
// database_id: i32,
// db: &mut SqliteConnection,
// ) -> Result<Value> {
// let video = get_video_from_db(database_id, db).await?;
// let video_json = serde_json::to_value(&video)?;
// let kind_json = serde_json::to_value(ServiceItemKind::Video)?;
// let json =
// serde_json::json!({"item": video_json, "kind": kind_json});
// Ok(json)
// }
async fn process_video(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
let video = get_video_from_db(database_id, db).await?;
let video_json = serde_json::to_value(&video)?;
let kind_json = serde_json::to_value(ServiceItemKind::Video)?;
let json = serde_json::json!({"item": video_json, "kind": kind_json});
Ok(json)
}
// async fn process_presentation(
// database_id: i32,
// db: &mut SqliteConnection,
// ) -> Result<Value> {
// let presentation =
// get_presentation_from_db(database_id, db).await?;
// let presentation_json = serde_json::to_value(&presentation)?;
// let kind_json = match presentation.kind {
// PresKind::Html => serde_json::to_value(
// ServiceItemKind::Presentation(PresKind::Html),
// )?,
// PresKind::Pdf => serde_json::to_value(
// ServiceItemKind::Presentation(PresKind::Pdf),
// )?,
// PresKind::Generic => serde_json::to_value(
// ServiceItemKind::Presentation(PresKind::Generic),
// )?,
// };
// let json = serde_json::json!({"item": presentation_json, "kind": kind_json});
// Ok(json)
// }
async fn process_presentation(database_id: i32, db: &mut SqliteConnection) -> Result<Value> {
let presentation = get_presentation_from_db(database_id, db).await?;
let presentation_json = serde_json::to_value(&presentation)?;
let kind_json = match presentation.kind {
PresKind::Html => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Html))?,
PresKind::Pdf => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Pdf))?,
PresKind::Generic => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Generic))?,
};
let json = serde_json::json!({"item": presentation_json, "kind": kind_json});
Ok(json)
}
// #[cfg(test)]
// mod test {
// use std::path::PathBuf;
#[cfg(test)]
mod test {
use std::path::PathBuf;
// use super::*;
// use fs::canonicalize;
// use pretty_assertions::assert_eq;
// use sqlx::Connection;
// use tracing::debug;
use fs::canonicalize;
use sqlx::Connection;
use pretty_assertions::assert_eq;
use tracing::debug;
use super::*;
// async fn get_db() -> SqliteConnection {
// let mut data = dirs::data_local_dir().unwrap();
// data.push("lumina");
// data.push("library-db.sqlite3");
// let mut db_url = String::from("sqlite://");
// db_url.push_str(data.to_str().unwrap());
// SqliteConnection::connect(&db_url).await.expect("problems")
// }
async fn get_db() -> SqliteConnection {
let mut data = dirs::data_local_dir().unwrap();
data.push("lumina");
data.push("library-db.sqlite3");
let mut db_url = String::from("sqlite://");
db_url.push_str(data.to_str().unwrap());
SqliteConnection::connect(&db_url)
.await
.expect("problems")
}
// #[tokio::test(flavor = "current_thread")]
// async fn test_process_song() {
// let mut db = get_db().await;
// let result = process_song(7, &mut db).await;
// let json_song_file = PathBuf::from("./test/test_song.json");
// if let Ok(path) = canonicalize(json_song_file) {
// debug!(file = ?&path);
// if let Ok(s) = fs::read_to_string(path) {
// debug!(s);
// match result {
// Ok(json) => assert_eq!(json.to_string(), s),
// Err(e) => panic!(
// "There was an error in processing the song: {e}"
// ),
// }
// } else {
// panic!("String wasn't read from file");
// }
// } else {
// panic!("Cannot find absolute path to test_song.json");
// }
// }
#[tokio::test(flavor = "current_thread")]
async fn test_process_song() {
let mut db = get_db().await;
let result = process_song(7, &mut db).await;
let json_song_file = PathBuf::from("./test/test_song.json");
if let Ok(path) = canonicalize(json_song_file) {
debug!(file = ?&path);
if let Ok(s) = fs::read_to_string(path) {
debug!(s);
match result {
Ok(json) => assert_eq!(json.to_string(), s),
Err(e) => panic!("There was an error in processing the song: {e}"),
}
} else {
panic!("String wasn't read from file");
}
} else {
panic!("Cannot find absolute path to test_song.json");
}
}
// #[tokio::test(flavor = "current_thread")]
// async fn test_process_image() {
// let mut db = get_db().await;
// let result = process_image(3, &mut db).await;
// let json_image_file = PathBuf::from("./test/test_image.json");
// if let Ok(path) = canonicalize(json_image_file) {
// debug!(file = ?&path);
// if let Ok(s) = fs::read_to_string(path) {
// debug!(s);
// match result {
// Ok(json) => assert_eq!(json.to_string(), s),
// Err(e) => panic!(
// "There was an error in processing the image: {e}"
// ),
// }
// } else {
// panic!("String wasn't read from file");
// }
// } else {
// panic!("Cannot find absolute path to test_image.json");
// }
// }
#[tokio::test(flavor = "current_thread")]
async fn test_process_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_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"
// );
// }
// }
#[tokio::test(flavor = "current_thread")]
async fn test_process_presentation() {
let mut db = get_db().await;
let result = process_presentation(54, &mut db).await;
let json_presentation_file = PathBuf::from("./test/test_presentation.json");
if let Ok(path) = canonicalize(json_presentation_file) {
debug!(file = ?&path);
if let Ok(s) = fs::read_to_string(path) {
debug!(s);
match result {
Ok(json) => assert_eq!(json.to_string(), s),
Err(e) => panic!("There was an error in processing the presentation: {e}"),
}
} else {
panic!("String wasn't read from file");
}
} else {
panic!("Cannot find absolute path to test_presentation.json");
}
}
// fn get_items() -> Vec<ServiceItem> {
// let items = vec![
// ServiceItem {
// database_id: 7,
// kind: ServiceItemKind::Song,
// id: 0,
// },
// ServiceItem {
// database_id: 54,
// kind: ServiceItemKind::Presentation(PresKind::Html),
// id: 0,
// },
// ServiceItem {
// database_id: 73,
// kind: ServiceItemKind::Video,
// id: 0,
// },
// ];
// items
// }
fn get_items() -> Vec<ServiceItem> {
let items = vec![
ServiceItem {
database_id: 7,
kind: ServiceItemKind::Song,
id: 0,
},
ServiceItem {
database_id: 54,
kind: ServiceItemKind::Presentation(PresKind::Html),
id: 0,
},
ServiceItem {
database_id: 73,
kind: ServiceItemKind::Video,
id: 0,
},
];
items
}
// #[tokio::test]
// async fn test_service_items() {
// let mut db = get_db().await;
// let items = get_items();
// let json_item_file =
// PathBuf::from("./test/test_service_items.json");
// let result = process_service_items(&items, &mut db).await;
// if let Ok(path) = canonicalize(json_item_file) {
// if let Ok(s) = fs::read_to_string(path) {
// match result {
// Ok(strings) => assert_eq!(strings.to_string(), s),
// Err(e) => panic!("There was an error: {e}"),
// }
// }
// }
// }
#[tokio::test]
async fn test_service_items() {
let mut db = get_db().await;
let items = get_items();
let json_item_file = PathBuf::from("./test/test_service_items.json");
let result = process_service_items(&items, &mut db).await;
if let Ok(path) = canonicalize(json_item_file) {
if let Ok(s) = fs::read_to_string(path) {
match result {
Ok(strings) => assert_eq!(strings.to_string(), s),
Err(e) => panic!("There was an error: {e}"),
}
}
}
}
// // #[tokio::test]
// // async fn test_save() {
// // let path = PathBuf::from("~/dev/lumina/src/rust/core/test.pres");
// // let list = get_items();
// // match save(list, path).await {
// // Ok(_) => assert!(true),
// // Err(e) => panic!("There was an error: {e}"),
// // }
// // }
// #[tokio::test]
// async fn test_save() {
// let path = PathBuf::from("~/dev/lumina/src/rust/core/test.pres");
// let list = get_items();
// match save(list, path).await {
// Ok(_) => assert!(true),
// Err(e) => panic!("There was an error: {e}"),
// }
// }
// #[tokio::test]
// async fn test_store() {
// let path = PathBuf::from(
// "/home/chris/dev/lumina/src/rust/core/test.pres",
// );
// let save_file = match File::create(path) {
// Ok(f) => f,
// Err(e) => panic!("Couldn't create save_file: {e}"),
// };
// let mut db = get_db().await;
// let list = get_items();
// if let Ok(json) = process_service_items(&list, &mut db).await
// {
// println!("{:?}", json);
// match store_service_items(
// &list, &mut db, &save_file, &json,
// )
// .await
// {
// Ok(_) => assert!(true),
// Err(e) => panic!("There was an error: {e}"),
// }
// } else {
// panic!("There was an error getting the json value");
// }
// }
#[tokio::test]
async fn test_store() {
let path = PathBuf::from("/home/chris/dev/lumina/src/rust/core/test.pres");
let save_file = match File::create(path) {
Ok(f) => f,
Err(e) => panic!("Couldn't create save_file: {e}"),
};
let mut db = get_db().await;
let list = get_items();
if let Ok(json) = process_service_items(&list, &mut db).await {
println!("{:?}", json);
match store_service_items(&list, &mut db, &save_file, &json).await {
Ok(_) => assert!(true),
Err(e) => panic!("There was an error: {e}"),
}
} else {
panic!("There was an error getting the json value");
}
}
// // #[tokio::test]
// // async fn test_things() {
// // let mut temp_dir = dirs::data_dir().unwrap();
// // temp_dir.push("lumina");
// // let mut s: String =
// // iter::repeat_with(fastrand::alphanumeric)
// // .take(5)
// // .collect();
// // s.insert_str(0, "temp_");
// // temp_dir.push(s);
// // let _ = fs::create_dir_all(&temp_dir);
// // let mut db = get_db().await;
// // let service_file = temp_dir.join("serviceitems.json");
// // let list = get_items();
// // if let Ok(json) = process_service_items(&list, &mut db).await {
// // let _ = fs::File::create(&service_file);
// // match fs::write(service_file, json.to_string()) {
// // Ok(_) => assert!(true),
// // Err(e) => panic!("There was an error: {e}"),
// // }
// // } else {
// // panic!("There was an error getting the json value");
// // }
// // }
// }
// #[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");
// }
// }
}

View file

@ -10,14 +10,14 @@ use crisp::types::{Keyword, Symbol, Value};
use miette::{IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};
use sqlx::{
Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
query, query_as,
pool::PoolConnection, query, query_as, Sqlite, SqliteConnection,
SqlitePool,
};
use std::path::{Path, PathBuf};
use tracing::{debug, error};
use std::path::PathBuf;
use tracing::error;
#[derive(
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
)]
pub struct Image {
pub id: i32,
@ -25,28 +25,6 @@ pub struct Image {
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 {
fn from(value: &Image) -> Self {
Self::List(vec![Self::Symbol(Symbol("image".into()))])
@ -74,9 +52,8 @@ impl Content for Image {
if self.path.exists() {
self.path
.file_name()
.map_or("Missing image".into(), |f| {
f.to_string_lossy().to_string()
})
.map(|f| f.to_string_lossy().to_string())
.unwrap_or("Missing image".into())
} else {
"Missing image".into()
}
@ -108,7 +85,7 @@ impl From<&Value> for Image {
let path =
p.to_str().unwrap_or_default().to_string();
let title =
path.rsplit_once('/').unwrap_or_default().1;
path.rsplit_once("/").unwrap_or_default().1;
title.to_string()
});
Self {
@ -177,49 +154,15 @@ impl Model<Image> {
.await;
match result {
Ok(v) => {
for image in v {
for image in v.into_iter() {
let _ = self.add_item(image);
}
}
Err(e) => {
error!(
"There was an error in converting images: {e}"
);
error!("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(
@ -229,29 +172,19 @@ pub async fn update_image_in_db(
let path = image
.path
.to_str()
.map(std::string::ToString::to_string)
.map(|s| s.to_string())
.unwrap_or_default();
let mut db = db.detach();
debug!(?image, "should be been updated");
let result = query!(
query!(
r#"UPDATE images SET title = $2, file_path = $3 WHERE id = $1"#,
image.id,
image.title,
path,
)
.execute(&mut db)
.await.into_diagnostic();
.execute(&mut db.detach())
.await
.into_diagnostic()?;
match result {
Ok(_) => {
debug!("should have been updated");
Ok(())
}
Err(e) => {
error! {?e};
Err(e)
}
}
}
pub async fn get_image_from_db(

View file

@ -1,11 +1,8 @@
use std::{error::Error, fmt::Display, path::PathBuf};
use std::{error::Error, fmt::Display};
use serde::{Deserialize, Serialize};
use crate::{
Slide,
core::{content::Content, service_items::ServiceItem},
};
use crate::Slide;
use super::{
images::Image, presentations::Presentation, songs::Song,
@ -21,65 +18,14 @@ pub enum ServiceItemKind {
Content(Slide),
}
impl TryFrom<PathBuf> for ServiceItemKind {
type Error = miette::Error;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
let ext = path
.extension()
.map(|ext| ext.to_str())
.flatten()
.ok_or(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)))
}
_ => Err(miette::miette!("Unknown item")),
}
}
}
impl ServiceItemKind {
pub fn title(&self) -> String {
match self {
ServiceItemKind::Song(song) => song.title.to_string(),
ServiceItemKind::Video(video) => video.title.to_string(),
ServiceItemKind::Image(image) => image.title.to_string(),
ServiceItemKind::Presentation(presentation) => {
presentation.title.to_string()
}
ServiceItemKind::Content(slide) => todo!(),
}
}
pub fn to_service_item(&self) -> ServiceItem {
match self {
ServiceItemKind::Song(song) => song.to_service_item(),
ServiceItemKind::Video(video) => video.to_service_item(),
ServiceItemKind::Image(image) => image.to_service_item(),
ServiceItemKind::Presentation(presentation) => {
presentation.to_service_item()
}
ServiceItemKind::Content(slide) => {
todo!()
}
}
}
}
impl std::fmt::Display for ServiceItemKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s = match self {
Self::Song(_) => "song".to_owned(),
Self::Image(_) => "image".to_owned(),
Self::Video(_) => "video".to_owned(),
Self::Presentation(_) => "html".to_owned(),
Self::Content(_) => "content".to_owned(),
Self::Song(s) => "song".to_owned(),
Self::Image(i) => "image".to_owned(),
Self::Video(v) => "video".to_owned(),
Self::Presentation(p) => "html".to_owned(),
Self::Content(s) => "content".to_owned(),
};
write!(f, "{s}")
}
@ -104,7 +50,7 @@ impl std::fmt::Display for ServiceItemKind {
// }
impl From<ServiceItemKind> for String {
fn from(val: ServiceItemKind) -> Self {
fn from(val: ServiceItemKind) -> String {
match val {
ServiceItemKind::Song(_) => "song".to_owned(),
ServiceItemKind::Video(_) => "video".to_owned(),
@ -130,9 +76,7 @@ impl Display for ParseError {
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let message = match self {
Self::UnknownType => {
"The type does not exist. It needs to be one of 'song', 'video', 'image', 'presentation', or 'content'"
}
Self::UnknownType => "The type does not exist. It needs to be one of 'song', 'video', 'image', 'presentation', or 'content'",
};
write!(f, "Error: {message}")
}

157
src/core/lisp.rs Normal file
View 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}");
// }
// }
// }
// }
}

View file

@ -1,11 +1,10 @@
pub mod content;
pub mod file;
pub mod images;
pub mod kinds;
pub mod lisp;
pub mod model;
pub mod presentations;
pub mod service_items;
pub mod settings;
pub mod slide;
pub mod songs;
pub mod thumbnail;

View file

@ -1,14 +1,7 @@
use std::{
borrow::Cow,
mem::replace,
path::{Path, PathBuf},
};
use std::mem::replace;
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use miette::{IntoDiagnostic, Result, miette};
use serde::{Deserialize, Serialize};
use miette::{miette, Result};
use sqlx::{Connection, SqliteConnection};
use tracing::debug;
#[derive(Debug, Clone)]
pub struct Model<T> {
@ -16,9 +9,7 @@ pub struct Model<T> {
pub kind: LibraryKind,
}
#[derive(
Debug, Clone, PartialEq, Eq, Copy, Hash, Serialize, Deserialize,
)]
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum LibraryKind {
Song,
Video,
@ -26,57 +17,6 @@ pub enum LibraryKind {
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> {
pub fn add_item(&mut self, item: T) -> Result<()> {
self.items.push(item);
@ -105,7 +45,6 @@ impl<T> Model<T> {
Ok(())
}
#[must_use]
pub fn get_item(&self, index: i32) -> Option<&T> {
self.items.get(index as usize)
}

View file

@ -1,14 +1,12 @@
use cosmic::widget::image::Handle;
use crisp::types::{Keyword, Symbol, Value};
use miette::{IntoDiagnostic, Result};
use mupdf::{Colorspace, Document, Matrix};
use serde::{Deserialize, Serialize};
use sqlx::{
Row, Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
prelude::FromRow, query, sqlite::SqliteRow,
pool::PoolConnection, prelude::FromRow, query, sqlite::SqliteRow,
Row, Sqlite, SqliteConnection, SqlitePool,
};
use std::path::{Path, PathBuf};
use tracing::{debug, error};
use std::path::PathBuf;
use tracing::error;
use crate::{Background, Slide, SlideBuilder, TextAlignment};
@ -24,15 +22,14 @@ use super::{
)]
pub enum PresKind {
Html,
Pdf {
starting_index: i32,
ending_index: i32,
},
#[default]
Pdf,
Generic,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub struct Presentation {
pub id: i32,
pub title: String,
@ -40,72 +37,8 @@ pub struct Presentation {
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" => {
if let Ok(document) = Document::open(&value.as_path())
{
if let Ok(count) = document.page_count() {
PresKind::Pdf {
starting_index: 0,
ending_index: count - 1,
}
} else {
PresKind::Pdf {
starting_index: 0,
ending_index: 0,
}
}
} else {
PresKind::Pdf {
starting_index: 0,
ending_index: 0,
}
}
}
"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 {
fn from(_value: &Presentation) -> Self {
fn from(value: &Presentation) -> Self {
Self::List(vec![Self::Symbol(Symbol("presentation".into()))])
}
}
@ -131,9 +64,8 @@ impl Content for Presentation {
if self.path.exists() {
self.path
.file_name()
.map_or("Missing presentation".into(), |f| {
f.to_string_lossy().to_string()
})
.map(|f| f.to_string_lossy().to_string())
.unwrap_or("Missing presentation".into())
} else {
"Missing presentation".into()
}
@ -185,56 +117,6 @@ impl ServiceTrait for Presentation {
}
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)| {
if (index as i32) < starting_index {
return None;
} else if (index as i32) > ending_index {
return None;
};
let Some(page) = page.ok() else {
return None;
};
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()
.background(
Background::try_from(self.path.clone())
@ -248,13 +130,9 @@ impl ServiceTrait for Presentation {
.video_loop(false)
.video_start_time(0.0)
.video_end_time(0.0)
.pdf_index(index as u32)
.pdf_page(page)
.build()?;
slides.push(slide);
}
debug!(?slides);
Ok(slides)
Ok(vec![slide])
}
fn box_clone(&self) -> Box<dyn ServiceTrait> {
@ -263,16 +141,14 @@ impl ServiceTrait for Presentation {
}
impl Presentation {
#[must_use]
pub fn new() -> Self {
Self {
title: String::new(),
title: "".to_string(),
..Default::default()
}
}
#[must_use]
pub const fn get_kind(&self) -> &PresKind {
pub fn get_kind(&self) -> &PresKind {
&self.kind
}
}
@ -289,10 +165,7 @@ impl FromRow<'_, SqliteRow> for Presentation {
kind: if row.try_get(3)? {
PresKind::Html
} else {
PresKind::Pdf {
starting_index: row.try_get(4)?,
ending_index: row.try_get(5)?,
}
PresKind::Pdf
},
})
}
@ -312,57 +185,21 @@ impl Model<Presentation> {
pub async fn load_from_db(&mut self, db: &mut SqliteConnection) {
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)
.await;
match result {
Ok(v) => {
for presentation in v {
for presentation in v.into_iter() {
let _ = self.add_item(Presentation {
id: presentation.id,
title: presentation.title,
path: presentation.path.clone().into(),
path: presentation.path.into(),
kind: if presentation.html {
PresKind::Html
} else {
if let (
Some(starting_index),
Some(ending_index),
) = (
presentation.starting_index,
presentation.ending_index,
) {
PresKind::Pdf {
starting_index: starting_index
as i32,
ending_index: ending_index as i32,
}
} else {
let path =
PathBuf::from(presentation.path);
if let Ok(document) =
Document::open(path.as_path())
{
if let Ok(count) =
document.page_count()
{
let ending_index = count - 1;
PresKind::Pdf {
starting_index: 0,
ending_index,
}
} else {
PresKind::Pdf {
starting_index: 0,
ending_index: 0,
}
}
} else {
PresKind::Generic
}
}
PresKind::Pdf
},
});
}
@ -374,40 +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();
query!(
r#"INSERT INTO presentations (title, file_path, html) VALUES ($1, $2, $3)"#,
presentation.title,
path,
html,
)
.execute(&mut db)
.await
.into_diagnostic()?;
Ok(())
}
pub async fn update_presentation_in_db(
presentation: Presentation,
db: PoolConnection<Sqlite>,
@ -415,84 +218,21 @@ pub async fn update_presentation_in_db(
let path = presentation
.path
.to_str()
.map(std::string::ToString::to_string)
.map(|s| s.to_string())
.unwrap_or_default();
let html = presentation.kind == PresKind::Html;
let mut db = db.detach();
let mut starting_index = 0;
let mut ending_index = 0;
if let PresKind::Pdf {
starting_index: s_index,
ending_index: e_index,
} = presentation.get_kind()
{
starting_index = *s_index;
ending_index = *e_index;
};
let id = presentation.id;
if let Err(e) =
query!("SELECT id FROM presentations where id = $1", id)
.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");
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!("should have been updated");
Ok(())
}
Err(e) => {
error! {?e};
Err(e)
}
};
} else {
return Err(miette::miette!("cannot find ids"));
}
};
debug!(?presentation, "should be been updated");
let result = query!(
query!(
r#"UPDATE presentations SET title = $2, file_path = $3, html = $4 WHERE id = $1"#,
presentation.id,
presentation.title,
path,
html
)
.execute(&mut db)
.await.into_diagnostic();
.execute(&mut db.detach())
.await
.into_diagnostic()?;
match result {
Ok(_) => {
debug!("should have been updated");
Ok(())
}
Err(e) => {
error! {?e};
Err(e)
}
}
}
pub async fn get_presentation_from_db(
@ -522,13 +262,7 @@ mod test {
#[test]
pub fn test_pres() {
let pres = Presentation::new();
assert_eq!(
pres.get_kind(),
&PresKind::Pdf {
starting_index: 0,
ending_index: 0,
}
)
assert_eq!(pres.get_kind(), &PresKind::Pdf)
}
#[tokio::test]

View file

@ -1,30 +1,29 @@
use std::borrow::Cow;
use std::cmp::Ordering;
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 miette::{IntoDiagnostic, Result, miette};
use serde::{Deserialize, Serialize};
// use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
use miette::Result;
use tracing::{debug, error};
use crate::Slide;
use super::images::Image;
use super::presentations::Presentation;
use super::songs::{Song, lisp_to_song};
use super::songs::{lisp_to_song, Song};
use super::videos::Video;
use super::kinds::ServiceItemKind;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Clone)]
pub struct ServiceItem {
pub id: i32,
pub title: String,
pub database_id: i32,
pub kind: ServiceItemKind,
pub slides: Vec<Slide>,
pub slides: Arc<[Slide]>,
// pub item: Box<dyn ServiceTrait>,
}
@ -48,73 +47,49 @@ impl TryFrom<(Vec<u8>, String)> for ServiceItem {
fn try_from(
value: (Vec<u8>, String),
) -> std::result::Result<Self, Self::Error> {
let (data, mime) = value;
debug!(?mime);
ron::de::from_bytes(&data).into_diagnostic()
debug!(?value);
let val = Value::from(
String::from_utf8(value.0)
.expect("Value couldn't be made"),
);
Ok(Self::from(&val))
}
}
impl AllowedMimeTypes for ServiceItem {
fn allowed() -> Cow<'static, [String]> {
Cow::from(vec![
"application/service-item".to_string(),
"text/uri-list".to_string(),
"x-special/gnome-copied-files".to_string(),
])
}
}
// impl AllowedMimeTypes for ServiceItem {
// fn allowed() -> Cow<'static, [String]> {
// Cow::from(vec!["application/service-item".to_string()])
// }
// }
impl AsMimeTypes for ServiceItem {
fn available(&self) -> Cow<'static, [String]> {
debug!(?self);
Cow::from(vec!["application/service-item".to_string()])
}
// impl AsMimeTypes for ServiceItem {
// 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 TryFrom<PathBuf> for ServiceItem {
type Error = miette::Error;
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
let ext = path
.extension()
.map(|ext| ext.to_str())
.flatten()
.ok_or(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")),
}
}
}
// fn as_bytes(
// &self,
// mime_type: &str,
// ) -> Option<std::borrow::Cow<'static, [u8]>> {
// debug!(?self);
// debug!(mime_type);
// let val = Value::from(self);
// let val = String::from(val);
// Some(Cow::from(val.into_bytes()))
// }
// }
impl From<&ServiceItem> for Value {
fn from(value: &ServiceItem) -> Self {
match &value.kind {
ServiceItemKind::Song(song) => Self::from(song),
ServiceItemKind::Video(video) => Self::from(video),
ServiceItemKind::Image(image) => Self::from(image),
ServiceItemKind::Song(song) => Value::from(song),
ServiceItemKind::Video(video) => Value::from(video),
ServiceItemKind::Image(image) => Value::from(image),
ServiceItemKind::Presentation(presentation) => {
Self::from(presentation)
Value::from(presentation)
}
ServiceItemKind::Content(slide) => Self::from(slide),
ServiceItemKind::Content(slide) => Value::from(slide),
}
}
}
@ -146,7 +121,7 @@ impl Default for ServiceItem {
title: String::default(),
database_id: 0,
kind: ServiceItemKind::Content(Slide::default()),
slides: vec![],
slides: Arc::new([]),
// item: Box::new(Image::default()),
}
}
@ -196,13 +171,13 @@ impl From<&Value> for ServiceItem {
kind: ServiceItemKind::Content(
slide.clone(),
),
slides: vec![slide],
slides: Arc::new([slide]),
}
} else if let Some(background) =
list.get(background_pos)
{
if let Value::List(item) = background {
match &item[0] {
match background {
Value::List(item) => match &item[0] {
Value::Symbol(Symbol(s))
if s == "image" =>
{
@ -225,29 +200,30 @@ impl From<&Value> for ServiceItem {
))
}
_ => todo!(),
}
} else {
},
_ => {
error!(
"There is no background here: {:?}",
background
);
Self::default()
ServiceItem::default()
}
}
} else {
error!(
"There is no background here: {:?}",
background_pos
);
Self::default()
ServiceItem::default()
}
}
Value::Symbol(Symbol(s)) if s == "song" => {
let song = lisp_to_song(list.clone());
Self::from(&song)
}
_ => Self::default(),
_ => ServiceItem::default(),
},
_ => Self::default(),
_ => ServiceItem::default(),
}
}
}
@ -286,7 +262,7 @@ impl From<&Song> for ServiceItem {
kind: ServiceItemKind::Song(song.clone()),
database_id: song.id,
title: song.title.clone(),
slides,
slides: slides.into(),
..Default::default()
}
} else {
@ -307,7 +283,7 @@ impl From<&Video> for ServiceItem {
kind: ServiceItemKind::Video(video.clone()),
database_id: video.id,
title: video.title.clone(),
slides,
slides: slides.into(),
..Default::default()
}
} else {
@ -328,7 +304,7 @@ impl From<&Image> for ServiceItem {
kind: ServiceItemKind::Image(image.clone()),
database_id: image.id,
title: image.title.clone(),
slides,
slides: slides.into(),
..Default::default()
}
} else {
@ -344,18 +320,17 @@ impl From<&Image> for ServiceItem {
impl From<&Presentation> for ServiceItem {
fn from(presentation: &Presentation) -> Self {
match presentation.to_slides() {
Ok(slides) => Self {
if let Ok(slides) = presentation.to_slides() {
Self {
kind: ServiceItemKind::Presentation(
presentation.clone(),
),
database_id: presentation.id,
title: presentation.title.clone(),
slides,
slides: slides.into(),
..Default::default()
},
Err(e) => {
error!(?e);
}
} else {
Self {
kind: ServiceItemKind::Presentation(
presentation.clone(),
@ -366,7 +341,6 @@ impl From<&Presentation> for ServiceItem {
}
}
}
}
}
impl Service {

View file

@ -1,77 +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,
)]
pub struct PersistentState {
pub recent_files: VecDeque<PathBuf>,
}
impl Default for PersistentState {
fn default() -> Self {
Self {
recent_files: VecDeque::new(),
}
}
}

View file

@ -1,8 +1,7 @@
use cosmic::widget::image::Handle;
// use cosmic::dialog::ashpd::url::Url;
// use iced::dialog::ashpd::url::Url;
use crisp::types::{Keyword, Symbol, Value};
use iced_video_player::Video;
use miette::{Result, miette};
use miette::{miette, Result};
use serde::{Deserialize, Serialize};
use std::{
fmt::Display,
@ -10,49 +9,10 @@ use std::{
};
use tracing::error;
use crate::ui::text_svg::TextSvg;
use crate::ui::text_svg::{self, TextSvg};
use super::songs::Song;
#[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,
pdf_index: u32,
#[serde(skip)]
pdf_page: Option<Handle>,
#[serde(skip)]
pub text_svg: Option<TextSvg>,
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub enum BackgroundKind {
#[default]
Image,
Video,
Pdf,
Html,
}
#[derive(Debug, Clone, Default)]
struct Image {
pub source: String,
pub fit: String,
pub children: Vec<String>,
}
#[derive(
Clone,
Copy,
@ -107,9 +67,9 @@ impl TryFrom<&Background> for Video {
fn try_from(
value: &Background,
) -> std::result::Result<Self, Self::Error> {
Self::new(
Video::new(
&url::Url::from_file_path(value.path.clone())
.map_err(|()| ParseError::BackgroundNotVideo)?,
.map_err(|_| ParseError::BackgroundNotVideo)?,
)
.map_err(|_| ParseError::BackgroundNotVideo)
}
@ -121,9 +81,9 @@ impl TryFrom<Background> for Video {
fn try_from(
value: Background,
) -> std::result::Result<Self, Self::Error> {
Self::new(
Video::new(
&url::Url::from_file_path(value.path)
.map_err(|()| ParseError::BackgroundNotVideo)?,
.map_err(|_| ParseError::BackgroundNotVideo)?,
)
.map_err(|_| ParseError::BackgroundNotVideo)
}
@ -132,7 +92,7 @@ impl TryFrom<Background> for Video {
impl TryFrom<String> for Background {
type Error = ParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
Background::try_from(value.as_str())
}
}
@ -147,7 +107,7 @@ impl TryFrom<PathBuf> for Background {
.to_str()
.unwrap()
.to_string();
let path = path.replace('~', &home);
let path = path.replace("~", &home);
PathBuf::from(path)
} else {
path
@ -161,27 +121,21 @@ impl TryFrom<PathBuf> for Background {
.to_str()
.unwrap_or_default();
match extension {
"jpeg" | "jpg" | "png" | "webp" => Ok(Self {
"jpeg" | "jpg" | "png" | "webp" | "html" => {
Ok(Self {
path: value,
kind: BackgroundKind::Image,
}),
})
}
"mp4" | "mkv" | "webm" => Ok(Self {
path: value,
kind: BackgroundKind::Video,
}),
"pdf" => Ok(Self {
path: value,
kind: BackgroundKind::Pdf,
}),
"html" => Ok(Self {
path: value,
kind: BackgroundKind::Html,
}),
_ => Err(ParseError::NonBackgroundFile),
}
}
Err(e) => {
// error!("Couldn't canonicalize: {e} {:?}", path);
error!("Couldn't canonicalize: {e} {:?}", path);
Err(ParseError::CannotCanonicalize)
}
}
@ -192,10 +146,10 @@ impl TryFrom<&str> for Background {
type Error = ParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim_start_matches("file://");
if value.starts_with('~') {
if value.starts_with("~") {
if let Some(home) = dirs::home_dir() {
if let Some(home) = home.to_str() {
let value = value.replace('~', home);
let value = value.replace("~", home);
Self::try_from(PathBuf::from(value))
} else {
Self::try_from(PathBuf::from(value))
@ -249,16 +203,43 @@ impl Display for ParseError {
}
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
)]
pub enum BackgroundKind {
#[default]
Image,
Video,
}
impl From<String> for BackgroundKind {
fn from(value: String) -> Self {
if value == "image" {
Self::Image
BackgroundKind::Image
} 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 {
fn from(value: &Slide) -> Self {
Self::List(vec![Self::Symbol(Symbol("slide".into()))])
@ -271,17 +252,12 @@ impl Slide {
self
}
pub fn with_text_svg(mut self, text_svg: TextSvg) -> Self {
self.text_svg = Some(text_svg);
self
}
pub fn set_font(mut self, font: impl AsRef<str>) -> Self {
self.font = font.as_ref().into();
self
}
pub const fn set_font_size(mut self, font_size: i32) -> Self {
pub fn set_font_size(mut self, font_size: i32) -> Self {
self.font_size = font_size;
self
}
@ -291,12 +267,7 @@ impl Slide {
self
}
pub const fn set_pdf_index(mut self, pdf_index: u32) -> Self {
self.pdf_index = pdf_index;
self
}
pub const fn background(&self) -> &Background {
pub fn background(&self) -> &Background {
&self.background
}
@ -304,11 +275,7 @@ impl Slide {
self.text.clone()
}
pub const fn text_alignment(&self) -> TextAlignment {
self.text_alignment
}
pub const fn font_size(&self) -> i32 {
pub fn font_size(&self) -> i32 {
self.font_size
}
@ -316,7 +283,7 @@ impl Slide {
self.font.clone()
}
pub const fn video_loop(&self) -> bool {
pub fn video_loop(&self) -> bool {
self.video_loop
}
@ -324,17 +291,9 @@ impl Slide {
self.audio.clone()
}
pub fn pdf_page(&self) -> Option<Handle> {
self.pdf_page.clone()
}
pub const fn pdf_index(&self) -> u32 {
self.pdf_index
}
pub fn song_slides(song: &Song) -> Result<Vec<Self>> {
let lyrics = song.get_lyrics()?;
let slides: Vec<Self> = lyrics
let slides: Vec<Slide> = lyrics
.iter()
.map(|l| {
let song = song.clone();
@ -358,7 +317,7 @@ impl Slide {
Ok(slides)
}
pub(crate) const fn set_index(&mut self, index: i32) {
pub(crate) fn set_index(&mut self, index: i32) {
self.id = index;
}
@ -381,7 +340,7 @@ impl From<&Value> for Slide {
fn from(value: &Value) -> Self {
match value {
Value::List(list) => lisp_to_slide(list),
_ => Self::default(),
_ => Slide::default(),
}
}
}
@ -404,7 +363,7 @@ fn lisp_to_slide(lisp: &Vec<Value>) -> Slide {
slide = slide.background(lisp_to_background(background));
} else {
slide = slide.background(Background::default());
}
};
let text_position = lisp.iter().position(|v| match v {
Value::List(vec) => {
@ -547,9 +506,6 @@ pub struct SlideBuilder {
video_loop: Option<bool>,
video_start_time: Option<f32>,
video_end_time: Option<f32>,
pdf_index: Option<u32>,
#[serde(skip)]
pdf_page: Option<Handle>,
#[serde(skip)]
text_svg: Option<TextSvg>,
}
@ -633,19 +589,6 @@ impl SlideBuilder {
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> {
let Some(background) = self.background else {
return Err(miette!("No background"));
@ -671,6 +614,7 @@ impl SlideBuilder {
let Some(video_end_time) = self.video_end_time else {
return Err(miette!("No video_end_time"));
};
if let Some(text_svg) = self.text_svg {
Ok(Slide {
background,
text,
@ -681,17 +625,49 @@ impl SlideBuilder {
video_loop,
video_start_time,
video_end_time,
text_svg: self.text_svg,
pdf_index: self.pdf_index.unwrap_or_default(),
pdf_page: self.pdf_page,
text_svg,
..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 {
Default::default()
Self {
..Default::default()
}
}
}

View file

@ -1,15 +1,15 @@
use std::{collections::HashMap, option::Option, path::PathBuf};
use crisp::types::{Keyword, Symbol, Value};
use miette::{IntoDiagnostic, Result, miette};
use miette::{miette, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};
use sqlx::{
Acquire, FromRow, Row, Sqlite, SqliteConnection, SqlitePool,
pool::PoolConnection, query, sqlite::SqliteRow,
pool::PoolConnection, query, sqlite::SqliteRow, FromRow, Row,
Sqlite, SqliteConnection, SqlitePool,
};
use tracing::error;
use crate::{Slide, SlideBuilder, core::slide};
use crate::{core::slide, Slide, SlideBuilder};
use super::{
content::Content,
@ -34,7 +34,6 @@ pub struct Song {
pub text_alignment: Option<TextAlignment>,
pub font: Option<String>,
pub font_size: Option<i32>,
pub stroke_size: Option<i32>,
}
impl From<&Song> for Value {
@ -128,9 +127,7 @@ impl FromRow<'_, SqliteRow> for Song {
})),
verse_order: Some({
let str: &str = row.try_get(0)?;
str.split(' ')
.map(std::string::ToString::to_string)
.collect()
str.split(' ').map(|s| s.to_string()).collect()
}),
background: {
let string: String = row.try_get(7)?;
@ -161,7 +158,6 @@ impl FromRow<'_, SqliteRow> for Song {
}),
font: row.try_get(6)?,
font_size: row.try_get(1)?,
stroke_size: None,
})
}
}
@ -253,7 +249,9 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
.position(|v| v == &Value::Keyword(Keyword::from("title")))
{
let pos = key_pos + 1;
list.get(pos).map_or(String::from("song"), String::from)
list.get(pos)
.map(String::from)
.unwrap_or(String::from("song"))
} else {
String::from("song")
};
@ -320,30 +318,30 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
let lyric = String::from(&lyric[1]);
let verse_title = match lyric_verse.as_str() {
"i1" => r"\n\nIntro 1\n",
"i2" => r"\n\nIntro 1\n",
"v1" => r"\n\nVerse 1\n",
"v2" => r"\n\nVerse 2\n",
"v3" => r"\n\nVerse 3\n",
"v4" => r"\n\nVerse 4\n",
"v5" => r"\n\nVerse 5\n",
"c1" => r"\n\nChorus 1\n",
"c2" => r"\n\nChorus 2\n",
"c3" => r"\n\nChorus 3\n",
"c4" => r"\n\nChorus 4\n",
"b1" => r"\n\nBridge 1\n",
"b2" => r"\n\nBridge 2\n",
"e1" => r"\n\nEnding 1\n",
"e2" => r"\n\nEnding 2\n",
"o1" => r"\n\nOther 1\n",
"o2" => r"\n\nOther 2\n",
"i1" => r#"\n\nIntro 1\n"#,
"i2" => r#"\n\nIntro 1\n"#,
"v1" => r#"\n\nVerse 1\n"#,
"v2" => r#"\n\nVerse 2\n"#,
"v3" => r#"\n\nVerse 3\n"#,
"v4" => r#"\n\nVerse 4\n"#,
"v5" => r#"\n\nVerse 5\n"#,
"c1" => r#"\n\nChorus 1\n"#,
"c2" => r#"\n\nChorus 2\n"#,
"c3" => r#"\n\nChorus 3\n"#,
"c4" => r#"\n\nChorus 4\n"#,
"b1" => r#"\n\nBridge 1\n"#,
"b2" => r#"\n\nBridge 2\n"#,
"e1" => r#"\n\nEnding 1\n"#,
"e2" => r#"\n\nEnding 2\n"#,
"o1" => r#"\n\nOther 1\n"#,
"o2" => r#"\n\nOther 2\n"#,
_ => "",
};
let lyric = format!("{verse_title}{lyric}");
let lyric = lyric.replace(
"\\n", r"
",
"\\n", r#"
"#,
);
lyrics.push(lyric);
}
@ -393,15 +391,15 @@ impl Model<Song> {
let result = query(r#"SELECT verse_order as "verse_order!", font_size as "font_size!: i32", background_type as "background_type!", horizontal_text_alignment as "horizontal_text_alignment!", vertical_text_alignment as "vertical_text_alignment!", title as "title!", font as "font!", background as "background!", lyrics as "lyrics!", ccli as "ccli!", author as "author!", audio as "audio!", id as "id: i32" from songs"#).fetch_all(db).await;
match result {
Ok(s) => {
for song in s {
for song in s.into_iter() {
match Song::from_row(&song) {
Ok(song) => {
let _ = self.add_item(song);
}
Err(e) => {
error!("Could not convert song: {e}");
}
error!("Could not convert song: {e}")
}
};
}
}
Err(e) => {
@ -411,65 +409,6 @@ impl Model<Song> {
}
}
pub async fn remove_from_db(
db: PoolConnection<Sqlite>,
id: i32,
) -> Result<()> {
query!("DELETE FROM songs WHERE id = $1", id)
.execute(&mut db.detach())
.await
.into_diagnostic()
.map(|_| ())
}
pub async fn add_song_to_db(
db: PoolConnection<Sqlite>,
) -> Result<Song> {
let mut db = db.detach();
let mut song = Song::default();
let verse_order = {
if let Some(vo) = song.verse_order.clone() {
vo.into_iter()
.map(|mut s| {
s.push(' ');
s
})
.collect::<String>()
} else {
String::new()
}
};
let audio = song
.audio
.clone()
.map(|a| a.to_str().unwrap_or_default().to_string());
let background = song
.background
.clone()
.map(|b| b.path.to_str().unwrap_or_default().to_string());
let res = query!(
r#"INSERT INTO songs (title, lyrics, author, ccli, verse_order, audio, font, font_size, background) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"#,
song.title,
song.lyrics,
song.author,
song.ccli,
verse_order,
audio,
song.font,
song.font_size,
background
)
.execute(&mut db)
.await
.into_diagnostic()?;
song.id = res.last_insert_rowid() as i32;
Ok(song)
}
pub async fn update_song_in_db(
item: Song,
db: PoolConnection<Sqlite>,
@ -485,7 +424,7 @@ pub async fn update_song_in_db(
})
.collect::<String>()
} else {
String::new()
String::from("")
}
};
@ -548,13 +487,13 @@ impl Song {
let verse_order = self.verse_order.clone();
let mut lyric_map = HashMap::new();
let mut verse_title = String::new();
let mut lyric = String::new();
let mut verse_title = String::from("");
let mut lyric = String::from("");
for (i, line) in raw_lyrics.split('\n').enumerate() {
if VERSE_KEYWORDS.contains(&line) {
if i != 0 {
lyric_map.insert(verse_title, lyric);
lyric = String::new();
lyric = String::from("");
verse_title = line.to_string();
} else {
verse_title = line.to_string();
@ -595,7 +534,7 @@ impl Song {
lyric_list.push(lyric.to_string());
} else {
// error!("NOT WORKING!");
}
};
}
// for lyric in lyric_list.iter() {
// debug!(lyric = ?lyric)
@ -686,27 +625,7 @@ You saved my soul"
let lyrics = song.get_lyrics();
match lyrics {
Ok(lyrics) => {
assert_eq!(
vec![
"From the Day\nI Am They",
"When You found me,\nI was so blind\nMy sin was before me,\nI was swallowed by pride",
"But out of the darkness,\nYou brought me to Your light\nYou showed me new mercy\nAnd opened up my eyes",
"From the day\nYou saved my soul\n'Til the very moment\nWhen I come home",
"I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul",
"Where brilliant light\nIs all around\nAnd endless joy\nIs the only sound",
"Oh, rest my heart\nForever now\nOh, in Your arms\nI'll always be found",
"From the day\nYou saved my soul\n'Til the very moment\nWhen I come home",
"I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul",
"My love is Yours\nMy heart is Yours\nMy life is Yours\nForever",
"My love is Yours\nMy heart is Yours\nMy life is Yours\nForever",
"From the day\nYou saved my soul\n'Til the very moment\nWhen I come home",
"I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul",
"From the day\nYou saved my soul\n'Til the very moment\nWhen I come home",
"I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul",
"Oh Oh Oh\nFrom the day\nYou saved my soul\n"
],
lyrics
);
assert_eq!(vec!["From the Day\nI Am They", "When You found me,\nI was so blind\nMy sin was before me,\nI was swallowed by pride", "But out of the darkness,\nYou brought me to Your light\nYou showed me new mercy\nAnd opened up my eyes", "From the day\nYou saved my soul\n'Til the very moment\nWhen I come home", "I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul", "Where brilliant light\nIs all around\nAnd endless joy\nIs the only sound", "Oh, rest my heart\nForever now\nOh, in Your arms\nI'll always be found", "From the day\nYou saved my soul\n'Til the very moment\nWhen I come home", "I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul", "My love is Yours\nMy heart is Yours\nMy life is Yours\nForever", "My love is Yours\nMy heart is Yours\nMy life is Yours\nForever", "From the day\nYou saved my soul\n'Til the very moment\nWhen I come home", "I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul", "From the day\nYou saved my soul\n'Til the very moment\nWhen I come home", "I'll sing, I'll dance,\nMy heart will overflow\nFrom the day\nYou saved my soul", "Oh Oh Oh\nFrom the day\nYou saved my soul\n"], lyrics);
}
Err(e) => {
assert!(false, "{:?}", e)
@ -792,8 +711,7 @@ You saved my soul"
background: Some(Background::try_from("file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg").unwrap()),
text_alignment: Some(TextAlignment::MiddleCenter),
font: Some("Quicksand Bold".to_string()),
font_size: Some(60),
stroke_size: None,
font_size: Some(60)
}
}

View file

@ -11,9 +11,7 @@ pub fn bg_from_video(
video: &Path,
screenshot: &Path,
) -> Result<(), Box<dyn Error>> {
if screenshot.exists() {
debug!("Screenshot already exists");
} else {
if !screenshot.exists() {
let output_duration = Command::new("ffprobe")
.args(["-i", &video.to_string_lossy()])
.output()
@ -28,9 +26,9 @@ pub fn bg_from_video(
let mut duration = log.split_off(duration_index + 10);
duration.truncate(11);
// debug!("rust-duration-is: {duration}");
let mut hours = String::new();
let mut minutes = String::new();
let mut seconds = String::new();
let mut hours = String::from("");
let mut minutes = String::from("");
let mut seconds = String::from("");
for (i, c) in duration.chars().enumerate() {
if i <= 1 {
hours.push(c);
@ -65,6 +63,8 @@ pub fn bg_from_video(
.expect("failed to execute ffmpeg");
// io::stdout().write_all(&output.stdout).unwrap();
// io::stderr().write_all(&output.stderr).unwrap();
} else {
debug!("Screenshot already exists");
}
Ok(())
}
@ -95,10 +95,7 @@ mod test {
let screenshot = bg_path_from_video(video);
let screenshot_string =
screenshot.to_str().expect("Should be thing");
assert_eq!(
screenshot_string,
"/home/chris/.local/share/lumina/thumbnails/moms-funeral.png"
);
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);
@ -121,9 +118,6 @@ mod test {
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"
);
assert_ne!(screenshot_string, "/home/chris/.local/share/lumina/thumbnails/All WebDev Sucks and you know it.webm");
}
}

View file

@ -11,11 +11,11 @@ use crisp::types::{Keyword, Symbol, Value};
use miette::{IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};
use sqlx::{
Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
query, query_as,
pool::PoolConnection, query, query_as, Sqlite, SqliteConnection,
SqlitePool,
};
use std::path::{Path, PathBuf};
use tracing::{debug, error};
use std::path::PathBuf;
use tracing::error;
#[derive(
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
@ -30,31 +30,11 @@ pub struct Video {
}
impl From<&Video> for Value {
fn from(_value: &Video) -> Self {
fn from(value: &Video) -> Self {
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 {
fn title(&self) -> String {
self.title.clone()
@ -76,9 +56,8 @@ impl Content for Video {
if self.path.exists() {
self.path
.file_name()
.map_or("Missing video".into(), |f| {
f.to_string_lossy().to_string()
})
.map(|f| f.to_string_lossy().to_string())
.unwrap_or("Missing video".into())
} else {
"Missing video".into()
}
@ -110,7 +89,7 @@ impl From<&Value> for Video {
let path =
p.to_str().unwrap_or_default().to_string();
let title =
path.rsplit_once('/').unwrap_or_default().1;
path.rsplit_once("/").unwrap_or_default().1;
title.to_string()
});
@ -144,7 +123,8 @@ impl From<&Value> for Video {
}) {
let pos = loop_pos + 1;
list.get(pos)
.is_some_and(|l| String::from(l) == *"true")
.map(|l| String::from(l) == *"true")
.unwrap_or_default()
} else {
false
};
@ -212,52 +192,15 @@ 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;
match result {
Ok(v) => {
for video in v {
for video in v.into_iter() {
let _ = self.add_item(video);
}
}
Err(e) => {
error!(
"There was an error in converting videos: {e}"
);
error!("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(
@ -267,11 +210,9 @@ pub async fn update_video_in_db(
let path = video
.path
.to_str()
.map(std::string::ToString::to_string)
.map(|s| s.to_string())
.unwrap_or_default();
let mut db = db.detach();
debug!(?video, "should be been updated");
let result = query!(
query!(
r#"UPDATE videos SET title = $2, file_path = $3, start_time = $4, end_time = $5, loop = $6 WHERE id = $1"#,
video.id,
video.title,
@ -280,19 +221,11 @@ pub async fn update_video_in_db(
video.end_time,
video.looping,
)
.execute(&mut db)
.await.into_diagnostic();
.execute(&mut db.detach())
.await
.into_diagnostic()?;
match result {
Ok(_) => {
debug!("should have been updated");
Ok(())
}
Err(e) => {
error! {?e};
Err(e)
}
}
}
pub async fn get_video_from_db(

View file

@ -40,11 +40,11 @@ mod test {
use std::{fs::read_to_string, path::PathBuf};
use crate::{
Background, TextAlignment,
core::{
images::Image, kinds::ServiceItemKind,
service_items::ServiceTrait, songs::Song, videos::Video,
},
Background, TextAlignment,
};
use super::*;

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
use std::ops::RangeInclusive;
use cosmic::iced::Length;
use iced::Length;
struct DoubleSlider<'a, T, Message> {
range: RangeInclusive<T>,

View file

@ -1,185 +0,0 @@
use std::{io, path::PathBuf};
use crate::core::images::Image;
use cosmic::{
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 {
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 = title.clone();
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| {
if let Ok(image) = image_result {
let mut image = Image::from(image);
image.id = image_id;
Message::Update(image)
} else {
Message::None
}
},
);
return Action::Task(task);
}
Message::None => (),
}
Action::None
}
pub fn view(&self) -> Element<Message> {
let container = if let Some(pic) = &self.image {
let image = widget::image(pic.path.clone());
container(image)
} else {
container(Space::new(0, 0))
};
let column = column![
self.toolbar(),
container.center_x(Length::FillPortion(2))
]
.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()
}
pub const fn editing(&self) -> bool {
self.editing
}
fn update_entire_image(&mut self, image: &Image) {
self.image = Some(image.clone());
self.title = image.title.clone();
}
}
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().unwrap())
// 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),
}

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,12 @@
use crate::core::model::LibraryKind;
pub mod double_ended_slider;
pub mod image_editor;
pub mod library;
pub mod presentation_editor;
pub mod presenter;
pub mod service;
pub mod slide_editor;
pub mod song_editor;
pub mod text_svg;
pub mod video;
pub mod video_editor;
pub mod widgets;
pub enum EditorMode {

View file

@ -1,526 +0,0 @@
use std::{
collections::HashMap,
io,
path::{Path, PathBuf},
};
use crate::core::presentations::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;
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),
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 {
MenuAction::SplitBefore => Message::SplitBefore,
MenuAction::SplitAfter => Message::SplitAfter,
}
}
}
impl PresentationEditor {
pub fn new() -> Self {
Self {
presentation: None,
document: None,
title: "".to_string(),
editing: false,
current_slide: None,
current_slide_index: None,
page_count: None,
slides: None,
hovered_slide: None,
context_menu_id: None,
}
}
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::ChangePresentation(presentation) => {
self.update_entire_presentation(&presentation);
if let Some(presentation) = self.presentation.clone()
{
let task = Task::perform(
get_all_pages(presentation.path.clone()),
|pages| Message::AddSlides(pages),
);
return Action::Task(task);
}
}
Message::ChangeTitle(title) => {
self.title = title.clone();
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| {
if let Ok(presentation) = presentation_result
{
let mut presentation =
Presentation::from(presentation);
presentation.id = presentation_id;
Message::ChangePresentationFile(
presentation,
)
} else {
Message::None
}
},
);
return Action::Task(task);
}
Message::ChangePresentationFile(presentation) => {
self.update_entire_presentation(&presentation);
if let Some(presentation) = self.presentation.clone()
{
let task = Task::perform(
get_all_pages(presentation.path.clone()),
|pages| Message::AddSlides(pages),
)
.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;
if next_index > self.page_count.unwrap_or_default() {
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;
if previous_index < 0 {
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(index as i32).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(index as i32);
}
Message::HoverSlide(slide) => {
self.hovered_slide = slide;
}
Message::ContextMenu(index) => {
self.context_menu_id = Some(index as i32);
}
Message::SplitBefore => {
if let Some(index) = self.context_menu_id {
debug!("split before {index}");
} else {
error!("split before no index");
}
}
Message::SplitAfter => {
if let Some(index) = self.context_menu_id {
debug!("split after {index}");
} else {
error!("split after no index");
}
}
}
Action::None
}
pub fn view(&self) -> Element<Message> {
let presentation = if let Some(slide) = &self.current_slide {
container(
widget::image(slide)
.content_fit(ContentFit::ScaleDown),
)
.style(|_| {
container::background(Background::Color(
cosmic::iced::Color::WHITE,
))
})
} else {
container(Space::new(0, 0))
};
let pdf_pages: Vec<Element<Message>> = if let Some(pages) =
&self.slides
{
pages
.iter()
.enumerate()
.map(|(index, page)| {
let image = widget::image(page)
.height(theme::spacing().space_xxxl * 3)
.content_fit(ContentFit::ScaleDown);
let slide = container(image).style(|_| {
container::background(Background::Color(
cosmic::iced::Color::WHITE,
))
});
let clickable_slide = container(
mouse_area(slide)
.on_enter(Message::HoverSlide(Some(
index as i32,
)))
.on_exit(Message::HoverSlide(None))
.on_right_press(Message::ContextMenu(
index,
))
.on_press(Message::ChangeSlide(index)),
)
.padding(theme::spacing().space_m)
.clip(true)
.class(
if let Some(hovered_index) =
self.hovered_slide
{
if index as i32 == hovered_index {
theme::Container::Primary
} else {
theme::Container::Card
}
} else {
theme::Container::Card
},
);
clickable_slide.into()
})
.collect()
} else {
vec![horizontal_space().into()]
};
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.title)
.on_input(Message::ChangeTitle);
let presentation_selector = button::icon(
icon::from_name("folder-presentations-symbolic").scale(2),
)
.label("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 = presentation.title.clone();
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");
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);
}
}
impl Default for PresentationEditor {
fn default() -> Self {
Self::new()
}
}
async fn get_all_pages(
presentation_path: impl AsRef<Path>,
) -> Option<Vec<Handle>> {
let document = Document::open(presentation_path.as_ref()).ok()?;
let pages = document.pages().ok()?;
Some(
pages
.filter_map(|page| {
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().unwrap())
// 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),
}

View file

@ -1,42 +1,34 @@
use miette::{IntoDiagnostic, Result};
use std::{
fs::File,
io::BufReader,
path::PathBuf,
sync::{Arc, LazyLock},
};
use std::{fs::File, io::BufReader, path::PathBuf, sync::Arc};
use cosmic::{
Task,
iced::{
Background, Border, Color, ContentFit, Font, Length, Shadow,
Vector,
use iced::{
alignment::Horizontal,
border,
font::{Family, Stretch, Style, Weight},
},
iced_widget::{
scrollable::{
AbsoluteOffset, Direction, Scrollbar, scroll_to,
},
stack, vertical_rule,
},
prelude::*,
widget::{
Container, Id, Row, Space, container, image, mouse_area,
responsive, scrollable, text,
container, image, mouse_area, responsive, rich_text,
scrollable::{
self, scroll_to, AbsoluteOffset, Direction, Id, Scrollbar,
},
span, stack, text, vertical_rule, Column, Container, Row,
Space,
},
Background, Border, Color, ContentFit, Element, Font, Length,
Shadow, Task, Vector,
};
use iced_video_player::{Position, Video, VideoPlayer, gst_pbutils};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink};
use iced_video_player::{Position, Video, VideoPlayer};
use rodio::{Decoder, OutputStream, Sink};
use tracing::{debug, error, info, warn};
use url::Url;
use crate::{
BackgroundKind,
core::{service_items::ServiceItem, slide::Slide},
// ui::widgets::slide_text,
BackgroundKind,
};
const REFERENCE_WIDTH: f32 = 1920.0;
static DEFAULT_SLIDE: LazyLock<Slide> = LazyLock::new(Slide::default);
const REFERENCE_HEIGHT: f32 = 1080.0;
// #[derive(Default, Clone, Debug)]
pub(crate) struct Presenter {
@ -49,7 +41,7 @@ pub(crate) struct Presenter {
pub video: Option<Video>,
pub video_position: f32,
pub audio: Option<PathBuf>,
sink: (Arc<Sink>, OutputStream),
sink: (OutputStream, Arc<Sink>),
hovered_slide: Option<(usize, usize)>,
scroll_id: Id,
current_font: Font,
@ -67,7 +59,6 @@ pub(crate) enum Message {
NextSlide,
PrevSlide,
SlideChange(Slide),
ActivateSlide(usize, usize),
EndVideo,
StartVideo,
StartAudio,
@ -134,9 +125,7 @@ impl Presenter {
Some(v)
}
Err(e) => {
error!(
"Had an error creating the video object: {e}, likely the first slide isn't a video"
);
error!("Had an error creating the video object: {e}, likely the first slide isn't a video");
None
}
}
@ -152,40 +141,27 @@ impl Presenter {
};
let total_slides: usize =
items.iter().fold(0, |a, item| a + item.slides.len());
let slide =
items.first().and_then(|item| item.slides.first());
let audio = items
.first()
.and_then(|item| {
item.slides.first().map(|slide| slide.audio())
})
.flatten();
Self {
current_slide: slide.unwrap_or(&DEFAULT_SLIDE).clone(),
current_slide: items[0].slides[0].clone(),
current_item: 0,
current_slide_index: 0,
absolute_slide_index: 0,
total_slides,
video,
audio,
audio: items[0].slides[0].audio().clone(),
service: items,
video_position: 0.0,
hovered_slide: None,
sink: {
let stream_handle =
OutputStreamBuilder::open_default_stream()
.expect("Can't open default rodio stream");
let (stream, stream_handle) =
OutputStream::try_default().unwrap();
(
Arc::new(Sink::connect_new(
stream_handle.mixer(),
)),
stream_handle,
stream,
Arc::new(Sink::try_new(&stream_handle).unwrap()),
)
},
scroll_id: Id::unique(),
current_font: cosmic::font::default(),
current_font: iced::font::Font::DEFAULT,
}
}
@ -215,75 +191,33 @@ impl Presenter {
// self.current_slide_index - 1,
// ));
}
Message::ActivateSlide(item_index, slide_index) => {
if let Some(slide) = self
.service
.get(item_index)
.and_then(|item| item.slides.get(slide_index))
{
self.current_item = item_index;
self.current_slide_index = slide_index;
return self
.update(Message::SlideChange(slide.clone()));
}
}
Message::SlideChange(slide) => {
let slide_text = slide.text();
debug!(slide_text, "slide changed");
let bg = slide.background().clone();
debug!(?bg, "comparing background...");
let backgrounds_match =
self.current_slide.background()
== slide.background();
debug!(?slide, "slide changed");
let old_background =
self.current_slide.background().clone();
// self.current_slide_index = slide;
debug!("cloning slide...");
self.current_slide = slide.clone();
let _ =
self.update(Message::ChangeFont(slide.font()));
debug!("changing video now...");
if !backgrounds_match {
if self.current_slide.background() != &old_background
{
if let Some(video) = &mut self.video {
let _ = video.restart_stream();
}
self.reset_video();
}
let mut target_item = 0;
self.service.iter().enumerate().try_for_each(
|(index, item)| {
item.slides.iter().enumerate().try_for_each(
|(slide_index, _)| {
target_item += 1;
if (index, slide_index)
== (
self.current_item,
self.current_slide_index,
)
{
None
} else {
Some(())
}
},
)
},
);
debug!(target_item);
let offset = AbsoluteOffset {
x: {
if target_item > 2 {
(target_item as f32)
.mul_add(187.5, -187.5)
if self.current_slide_index > 2 {
self.current_slide_index as f32 * 187.5
- 187.5
} else {
0.0
}
},
y: 0.0,
};
let mut tasks = vec![];
tasks.push(scroll_to(self.scroll_id.clone(), offset));
@ -305,12 +239,12 @@ impl Presenter {
Some(current_audio)
if current_audio != *new_audio =>
{
self.audio = Some(new_audio.clone());
debug!(
?new_audio,
?current_audio,
"audio needs to change"
);
self.audio = Some(new_audio);
tasks.push(self.start_audio());
}
Some(current_audio) => {
@ -325,10 +259,10 @@ impl Presenter {
?new_audio,
"could not find audio, need to change"
);
self.audio = Some(new_audio);
self.audio = Some(new_audio.clone());
tasks.push(self.start_audio());
}
}
};
} else {
self.audio = None;
self.update(Message::EndAudio);
@ -380,7 +314,7 @@ impl Presenter {
std::time::Duration::from_secs_f32(position),
);
match video.seek(position, false) {
Ok(()) => debug!(
Ok(_) => debug!(
"Video position changed: {:?}",
position
),
@ -403,27 +337,27 @@ impl Presenter {
return Action::Task(Task::perform(
async move {
tokio::task::spawn_blocking(move || {
match gst_pbutils::MissingPluginMessage::parse(&element) {
Ok(missing_plugin) => {
let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
install_ctx
.set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina"));
let install_detail = missing_plugin.installer_detail();
println!("installing plugins: {install_detail}");
let status = gst_pbutils::missing_plugins::install_plugins_sync(
&[&install_detail],
Some(&install_ctx),
);
info!("plugin install status: {}", status);
info!(
"gstreamer registry update: {:?}",
gstreamer::Registry::update()
);
}
Err(err) => {
warn!("failed to parse missing plugin message: {err}");
}
}
// match gst_pbutils::MissingPluginMessage::parse(&element) {
// Ok(missing_plugin) => {
// let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
// install_ctx
// .set_desktop_id(&format!("{}.desktop", "org.chriscochrun.lumina"));
// let install_detail = missing_plugin.installer_detail();
// println!("installing plugins: {}", install_detail);
// let status = gst_pbutils::missing_plugins::install_plugins_sync(
// &[&install_detail],
// Some(&install_ctx),
// );
// info!("plugin install status: {}", status);
// info!(
// "gstreamer registry update: {:?}",
// gstreamer::Registry::update()
// );
// }
// Err(err) => {
// warn!("failed to parse missing plugin message: {err}");
// }
// }
Message::None
})
.await
@ -436,25 +370,37 @@ impl Presenter {
self.hovered_slide = slide;
}
Message::StartAudio => {
return Action::Task(self.start_audio());
return Action::Task(self.start_audio())
}
Message::EndAudio => {
self.sink.0.stop();
self.sink.1.stop();
}
Message::None => debug!("Presenter Message::None"),
Message::Error(error) => {
error!(error);
}
}
};
Action::None
}
pub fn view(&self) -> Element<Message> {
slide_view(&self.current_slide, &self.video, false, true)
slide_view(
self.current_slide.clone(),
&self.video,
self.current_font,
false,
true,
)
}
pub fn view_preview(&self) -> Element<Message> {
slide_view(&self.current_slide, &self.video, false, false)
slide_view(
self.current_slide.clone(),
&self.video,
self.current_font,
false,
false,
)
}
pub fn preview_bar(&self) -> Element<Message> {
@ -464,6 +410,19 @@ impl Presenter {
let mut slides = vec![];
item.slides.iter().enumerate().for_each(
|(slide_index, slide)| {
let font_name = slide.font().into_boxed_str();
let family =
Family::Name(Box::leak(font_name));
let weight = Weight::Normal;
let stretch = Stretch::Normal;
let style = Style::Normal;
let font = Font {
family,
weight,
stretch,
style,
};
let is_current_slide =
(item_index, slide_index)
== (
@ -472,8 +431,9 @@ impl Presenter {
);
let container = slide_view(
slide,
slide.clone(),
&self.video,
font,
true,
false,
);
@ -482,7 +442,7 @@ impl Presenter {
.style(move |t| {
let mut style =
container::Style::default();
let theme = t.cosmic();
let theme = t;
let hovered = self.hovered_slide
== Some((
item_index,
@ -492,19 +452,25 @@ impl Presenter {
Some(Background::Color(
if is_current_slide {
theme
.accent
.base
.into()
.extended_palette(
)
.secondary
.strong
.color
} else if hovered {
theme
.accent
.hover
.into()
.extended_palette(
)
.secondary
.strong
.color
} else {
theme
.palette
.neutral_3
.into()
.extended_palette(
)
.background
.neutral
.color
},
));
style.border = Border::default()
@ -537,7 +503,7 @@ impl Presenter {
.padding(10),
)
.interaction(
cosmic::iced::mouse::Interaction::Pointer,
iced::mouse::Interaction::Pointer,
)
.on_move(move |_| {
Message::HoveredSlide(Some((
@ -546,9 +512,8 @@ impl Presenter {
)))
})
.on_exit(Message::HoveredSlide(None))
.on_press(Message::ActivateSlide(
item_index,
slide_index,
.on_press(Message::SlideChange(
slide.clone(),
));
slides.push(delegate.into());
},
@ -556,11 +521,11 @@ impl Presenter {
let row = Row::from_vec(slides)
.spacing(10)
.padding([20, 15]);
let label = text::body(item.title.clone());
let label = text(item.title.clone());
let label_container = container(label)
.align_top(Length::Fill)
.align_left(Length::Fill)
.padding([0, 0, 0, 35]);
.padding([0, 35]);
let divider = vertical_rule(2);
items.push(
container(stack!(row, label_container))
@ -570,11 +535,12 @@ impl Presenter {
items.push(divider.into());
},
);
let row =
scrollable(container(Row::from_vec(items)).style(|t| {
let row = scrollable::Scrollable::new(
container(Row::from_vec(items)).style(|t| {
let style = container::Style::default();
style.border(Border::default().width(2))
}))
}),
)
.direction(Direction::Horizontal(Scrollbar::new()))
.height(Length::Fill)
.width(Length::Fill)
@ -612,7 +578,7 @@ impl Presenter {
// Container::new(container)
// .style(move |t| {
// let mut style = container::Style::default();
// let theme = t.cosmic();
// let theme = t.iced();
// let hovered = self.hovered_slide == slide_id;
// style.background = Some(Background::Color(
// if is_current_slide {
@ -651,7 +617,7 @@ impl Presenter {
// .height(100)
// .padding(10),
// )
// .interaction(cosmic::iced::mouse::Interaction::Pointer)
// .interaction(iced::iced::mouse::Interaction::Pointer)
// .on_move(move |_| Message::HoveredSlide(slide_id))
// .on_exit(Message::HoveredSlide(-1))
// .on_press(Message::SlideChange(slide.clone()));
@ -660,6 +626,7 @@ impl Presenter {
fn reset_video(&mut self) {
match self.current_slide.background().kind {
BackgroundKind::Image => self.video = None,
BackgroundKind::Video => {
let path = &self.current_slide.background().path;
if path.exists() {
@ -670,12 +637,10 @@ impl Presenter {
v.set_looping(
self.current_slide.video_loop(),
);
self.video = Some(v);
self.video = Some(v)
}
Err(e) => {
error!(
"Had an error creating the video object: {e}"
);
error!("Had an error creating the video object: {e}");
self.video = None;
}
}
@ -683,7 +648,6 @@ impl Presenter {
self.video = None;
}
}
_ => self.video = None,
}
}
@ -692,21 +656,14 @@ impl Presenter {
debug!(?audio, "This is where audio should be changing");
let audio = audio.clone();
Task::perform(
start_audio(Arc::clone(&self.sink.0), audio),
|()| Message::None,
start_audio(Arc::clone(&self.sink.1), audio),
|_| Message::None,
)
} else {
debug!(?self.audio, "Apparently this doesn't exist");
Task::none()
}
}
pub fn update_items(&mut self, items: Vec<ServiceItem>) {
let total_slides: usize =
items.iter().fold(0, |a, item| a + item.slides.len());
self.service = items;
self.total_slides = total_slides;
}
}
// This needs to be async so that rodio's audio will work
@ -733,29 +690,111 @@ fn scale_font(font_size: f32, width: f32) -> f32 {
}
}
pub(crate) fn slide_view<'a>(
slide: &'a Slide,
video: &'a Option<Video>,
pub(crate) fn slide_view(
slide: Slide,
video: &Option<Video>,
font: Font,
delegate: bool,
hide_mouse: bool,
) -> Element<'a, Message> {
) -> Element<'_, Message> {
let res = responsive(move |size| {
let width = size.height * 16.0 / 9.0;
let slide_text = slide.text();
let text: Element<Message> =
if let Some(text) = &slide.text_svg {
if let Some(handle) = &text.handle {
image(handle)
.content_fit(ContentFit::ScaleDown)
.width(Length::Shrink)
.height(Length::Shrink)
// let font = SvgFont::from(font).size(font_size.floor() as u8);
let text_container = if delegate {
// text widget based
let font_size =
scale_font(slide.font_size() as f32, width);
let lines = slide_text.lines();
let text: Vec<Element<Message>> = lines
.map(|t| {
rich_text::<
'_,
&str,
Message,
iced::Theme,
iced::Renderer,
>([span(format!("{}\n", t))
.background(
Background::Color(Color::BLACK)
.scale_alpha(0.4),
)
.border(border::rounded(10))
.padding(10)])
.size(font_size)
.font(font)
.center()
.into()
// let chars: Vec<Span> = t
// .chars()
// .map(|c| -> Span {
// let character: String = format!("{}/n", c);
// span(character)
// .size(font_size)
// .font(font)
// .background(
// Background::Color(Color::BLACK)
// .scale_alpha(0.4),
// )
// .border(border::rounded(10))
// .padding(10)
})
.collect();
let text = Column::with_children(text).spacing(26);
Container::new(text)
.center(Length::Fill)
.align_x(Horizontal::Left)
} else {
Space::with_width(0).into()
}
} else {
Space::with_width(0).into()
// SVG based
let text = slide.text_svg.view().map(|m| Message::None);
Container::new(text)
.center(Length::Fill)
.align_x(Horizontal::Left)
// text widget based
// let font_size =
// scale_font(slide.font_size() as f32, width);
// let lines = slide_text.lines();
// let text: Vec<Element<Message>> = lines
// .map(|t| {
// rich_text([span(format!("{}\n", t))
// .background(
// Background::Color(Color::BLACK)
// .scale_alpha(0.4),
// )
// .border(border::rounded(10))
// .padding(10)])
// .size(font_size)
// .font(font)
// .center()
// .into()
// // let chars: Vec<Span> = t
// // .chars()
// // .map(|c| -> Span {
// // let character: String = format!("{}/n", c);
// // span(character)
// // .size(font_size)
// // .font(font)
// // .background(
// // Background::Color(Color::BLACK)
// // .scale_alpha(0.4),
// // )
// // .border(border::rounded(10))
// // .padding(10)
// })
// .collect();
// let text = Column::with_children(text).spacing(26);
// Container::new(text)
// .center(Length::Fill)
// .align_x(Horizontal::Left)
};
// let stroke_text_container = Container::new(stroke_text)
// .center(Length::Fill)
// .align_x(Horizontal::Left);
// let text_stack =
// stack!(stroke_text_container, text_container);
let black = Container::new(Space::new(0, 0))
.style(|_| {
container::background(Background::Color(Color::BLACK))
@ -763,7 +802,7 @@ pub(crate) fn slide_view<'a>(
.clip(true)
.width(width)
.height(size.height);
let background = match slide.background().kind {
let container = match slide.background().kind {
BackgroundKind::Image => {
let path = slide.background().path.clone();
Container::new(
@ -790,46 +829,32 @@ pub(crate) fn slide_view<'a>(
} else if let Some(video) = &video {
Container::new(
VideoPlayer::new(video)
.mouse_hidden(hide_mouse)
// .mouse_hidden(hide_mouse)
.width(width)
.height(size.height)
.on_end_of_stream(Message::EndVideo)
.on_new_frame(Message::VideoFrame)
.on_missing_plugin(Message::MissingPlugin)
.on_warning(|w| {
Message::Error(w.to_string())
})
// .on_missing_plugin(Message::MissingPlugin)
// .on_warning(|w| {
// Message::Error(w.to_string())
// })
.on_error(|e| {
Message::Error(e.to_string())
})
.content_fit(ContentFit::Cover),
)
.center(Length::Fill)
.center(Length::Shrink)
.clip(true)
// Container::new(Space::new(0, 0))
} else {
Container::new(Space::new(0, 0))
}
}
BackgroundKind::Pdf => {
if let Some(pdf) = slide.pdf_page() {
Container::new(
image(pdf).content_fit(ContentFit::Contain),
)
.center(Length::Fill)
.clip(true)
} else {
Container::new(Space::new(0.0, 0.0))
.center(Length::Fill)
.clip(true)
}
}
BackgroundKind::Html => todo!(),
};
let stack = stack!(
black,
background.center_x(Length::Fill),
container(text).center(Length::Fill)
container.center(Length::Fill),
text_container
);
Container::new(stack).center(Length::Fill).into()
});

View file

@ -1,349 +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;
pub fn service<'a, Message: Clone + 'static>(
service: &'a Vec<ServiceItem>,
) -> Service<'a, Message> {
Service::new(service)
}
pub struct Service<'a, Message> {
service: &'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> {
pub fn new(service: &'a Vec<ServiceItem>) -> Self {
Self {
service,
drag_threshold: 8.0,
on_start: None,
on_cancelled: None,
on_finish: None,
width: Length::Fill,
height: Length::Fill,
}
}
#[must_use]
pub 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,
// );
// }
pub fn on_start(mut self, on_start: Option<Message>) -> Self {
self.on_start = on_start;
self
}
pub fn on_cancel(
mut self,
on_cancelled: Option<Message>,
) -> Self {
self.on_cancelled = on_cancelled;
self
}
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.service {}
}
// 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>) -> Element<'a, Message> {
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()
}
}

View file

@ -1,13 +1,12 @@
use std::{io, path::PathBuf};
use cosmic::{
Renderer,
iced::{Color, Font, Length, Size},
use iced::{
widget::{
self,
canvas::{self, Program, Stroke},
container,
container, Canvas,
},
Color, Font, Length, Renderer, Size,
};
use tracing::debug;
@ -50,14 +49,14 @@ pub enum SlideError {
#[derive(Debug, Default)]
struct EditorProgram {
mouse_button_pressed: Option<cosmic::iced::mouse::Button>,
mouse_button_pressed: Option<iced::mouse::Button>,
}
impl SlideEditor {
pub fn view(
&self,
pub fn view<'a>(
&'a self,
font: Font,
) -> cosmic::Element<'_, SlideWidget> {
) -> iced::Element<'a, SlideWidget> {
container(
widget::canvas(&self.program)
.height(Length::Fill)
@ -67,9 +66,9 @@ 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
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
impl<'a> Program<SlideWidget, iced::Theme, iced::Renderer>
for EditorProgram
{
type State = ();
@ -78,9 +77,9 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
&self,
state: &Self::State,
renderer: &Renderer,
theme: &cosmic::Theme,
bounds: cosmic::iced::Rectangle,
cursor: cosmic::iced_core::mouse::Cursor,
theme: &iced::Theme,
bounds: iced::Rectangle,
cursor: iced::mouse::Cursor,
) -> Vec<canvas::Geometry<Renderer>> {
// We prepare a new `Frame`
let mut frame = canvas::Frame::new(renderer, bounds.size());
@ -89,7 +88,7 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
// We create a `Path` representing a simple circle
let circle = canvas::Path::circle(frame.center(), 50.0);
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),
);
@ -115,21 +114,19 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
fn update(
&self,
_state: &mut Self::State,
event: canvas::Event,
bounds: cosmic::iced::Rectangle,
_cursor: cosmic::iced_core::mouse::Cursor,
) -> (canvas::event::Status, Option<SlideWidget>) {
event: &iced::Event,
bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> std::option::Option<iced::widget::Action<SlideWidget>> {
match event {
canvas::Event::Mouse(event) => match event {
cosmic::iced::mouse::Event::CursorEntered => {
debug!("cursor entered");
iced::Event::Mouse(event) => match event {
iced::mouse::Event::CursorEntered => {
debug!("cursor entered")
}
cosmic::iced::mouse::Event::CursorLeft => {
debug!("cursor left");
iced::mouse::Event::CursorLeft => {
debug!("cursor left")
}
cosmic::iced::mouse::Event::CursorMoved {
position,
} => {
iced::mouse::Event::CursorMoved { position } => {
if bounds.x < position.x
&& bounds.y < position.y
&& (bounds.width + bounds.x) > position.x
@ -138,29 +135,34 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
debug!(?position, "cursor moved");
}
}
cosmic::iced::mouse::Event::ButtonPressed(button) => {
iced::mouse::Event::ButtonPressed(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"),
canvas::Event::Keyboard(event) => debug!("test"),
iced::Event::Touch(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(
&self,
_state: &Self::State,
_bounds: cosmic::iced::Rectangle,
_cursor: cosmic::iced_core::mouse::Cursor,
) -> cosmic::iced_core::mouse::Interaction {
cosmic::iced_core::mouse::Interaction::default()
_bounds: iced::Rectangle,
_cursor: iced::mouse::Cursor,
) -> iced::mouse::Interaction {
iced::mouse::Interaction::default()
}
}

View file

@ -1,36 +1,36 @@
use std::{io, path::PathBuf, sync::Arc};
use std::{io, path::PathBuf};
use cosmic::{
Apply, Element, Task,
dialog::file_chooser::{FileFilter, open::Dialog},
iced::{Color, Length, alignment::Vertical},
iced_wgpu::graphics::text::cosmic_text::fontdb,
iced_widget::{column, row},
theme,
widget::{
button, color_picker, combo_box, container, horizontal_space,
icon, progress_bar, scrollable, spin_button, text,
text_editor, text_input, tooltip,
},
};
use dirs::font_dir;
use iced::{
advanced::graphics::text::cosmic_text::fontdb,
font::{Family, Stretch, Style, Weight},
widget::{
button, column, combo_box, container, horizontal_space, row,
scrollable, text, text_editor, text_input, tooltip,
TextInput,
},
Element, Font, Length, Task,
};
use iced_video_player::Video;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use tracing::{debug, error};
use crate::{
Background, BackgroundKind,
core::{service_items::ServiceTrait, slide::Slide, songs::Song},
core::{service_items::ServiceTrait, songs::Song},
ui::{
presenter::slide_view, slide_editor::SlideEditor, text_svg,
slide_editor::{self, SlideEditor},
widgets::icon,
},
Background, BackgroundKind,
};
use super::presenter::slide_view;
#[derive(Debug)]
pub struct SongEditor {
pub song: Option<Song>,
title: String,
font_db: Arc<fontdb::Database>,
font_db: fontdb::Database,
fonts: Vec<(fontdb::ID, String)>,
fonts_combo: combo_box::State<String>,
font_sizes: combo_box::State<String>,
font: String,
@ -42,11 +42,9 @@ pub struct SongEditor {
editing: bool,
background: Option<Background>,
video: Option<Video>,
current_font: Font,
ccli: String,
song_slides: Option<Vec<Slide>>,
slide_state: SlideEditor,
stroke_sizes: combo_box::State<i32>,
stroke_size: i32,
}
pub enum Action {
@ -65,26 +63,35 @@ pub enum Message {
ChangeVerseOrder(String),
ChangeLyrics(text_editor::Action),
ChangeBackground(Result<PathBuf, SongError>),
UpdateSlides(Vec<Slide>),
PickBackground,
Edit(bool),
None,
ChangeAuthor(String),
PauseVideo,
UpdateStrokeSize(i32),
}
impl SongEditor {
pub fn new(font_db: Arc<fontdb::Database>) -> Self {
pub fn new() -> Self {
let fonts = font_dir();
debug!(?fonts);
let mut fonts: Vec<String> = font_db
let mut font_db = fontdb::Database::new();
font_db.load_system_fonts();
let mut fonts: Vec<(fontdb::ID, String)> = font_db
.faces()
.map(|f| f.families[0].0.clone())
.map(|f| {
let id = f.id;
let font_base_name: String =
f.families.iter().map(|f| f.0.clone()).collect();
let font_weight = f.weight;
let font_style = f.style;
let font_stretch = f.stretch;
(id, font_base_name)
})
.collect();
fonts.dedup();
fonts.sort();
let stroke_sizes = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// let fonts = vec![
// String::from("Quicksand"),
// String::from("Noto Sans"),
// ];
let font_sizes = vec![
"5".to_string(),
"6".to_string(),
@ -106,50 +113,40 @@ impl SongEditor {
"65".to_string(),
"70".to_string(),
"80".to_string(),
"90".to_string(),
"100".to_string(),
"110".to_string(),
"120".to_string(),
"130".to_string(),
"140".to_string(),
"150".to_string(),
"160".to_string(),
"170".to_string(),
];
let font_texts = fonts.iter().map(|f| f.1.clone()).collect();
Self {
song: None,
font_db,
fonts_combo: combo_box::State::new(fonts),
title: "Death was Arrested".to_string(),
font: "Quicksand".to_string(),
fonts,
fonts_combo: combo_box::State::new(font_texts),
title: "Death was Arrested".to_owned(),
font: "Quicksand".to_owned(),
font_size: 16,
font_sizes: combo_box::State::new(font_sizes),
verse_order: "Death was Arrested".to_string(),
verse_order: "Death was Arrested".to_owned(),
lyrics: text_editor::Content::new(),
editing: false,
author: "North Point Worship".into(),
audio: PathBuf::new(),
background: None,
video: None,
ccli: "8".to_string(),
current_font: iced::font::Font::DEFAULT,
ccli: "8".to_owned(),
slide_state: SlideEditor::default(),
song_slides: None,
stroke_sizes: combo_box::State::new(stroke_sizes),
stroke_size: 0,
}
}
pub fn update(&mut self, message: Message) -> Action {
match message {
Message::ChangeSong(song) => {
self.song = Some(song.clone());
let song_slides = song.clone().to_slides();
self.title = song.title;
if let Some(font) = song.font {
self.font = font;
}
self.font = font
};
if let Some(font_size) = song.font_size {
self.font_size = font_size as usize;
}
self.font_size = font_size as usize
};
if let Some(verse_order) = song.verse_order {
self.verse_order = verse_order
.into_iter()
@ -160,58 +157,51 @@ impl SongEditor {
.collect();
}
if let Some(author) = song.author {
self.author = author;
}
self.author = author
};
if let Some(audio) = song.audio {
self.audio = audio;
}
self.audio = audio
};
if let Some(ccli) = song.ccli {
self.ccli = ccli;
}
self.ccli = ccli
};
if let Some(lyrics) = song.lyrics {
self.lyrics =
text_editor::Content::with_text(&lyrics);
}
text_editor::Content::with_text(&lyrics)
};
self.background_video(&song.background);
self.background = song.background.clone();
let font_db = Arc::clone(&self.font_db);
let task = Task::perform(
async move {
song_slides
.ok()
.map(move |v| {
v.into_par_iter()
.map(move |mut s| {
text_svg::text_svg_generator(
&mut s,
Arc::clone(&font_db),
);
s
})
.collect::<Vec<Slide>>()
})
.unwrap_or_default()
},
|slides| Message::UpdateSlides(slides),
);
return Action::Task(task);
self.background = song.background;
}
Message::ChangeFont(font) => {
let font_id = self
.fonts
.iter()
.filter(|f| f.1 == font)
.map(|f| f.0)
.next();
if let Some(id) = font_id
&& let Some(face) = self.font_db.face(id)
{
self.font = face.post_script_name.clone();
// self.current_font = Font::from(face);
}
self.font = font.clone();
if let Some(song) = &mut self.song {
song.font = Some(font);
let song = song.to_owned();
return self.update_song(song);
}
}
Message::ChangeFontSize(size) => {
self.font_size = size;
if let Some(song) = &mut self.song {
song.font_size = Some(size as i32);
let song = song.to_owned();
return self.update_song(song);
}
let font_name = font.into_boxed_str();
let family = Family::Name(Box::leak(font_name));
let weight = Weight::Bold;
let stretch = Stretch::Normal;
let style = Style::Normal;
let font = Font {
family,
weight,
stretch,
style,
};
self.current_font = font;
// return self.update_song(song);
}
Message::ChangeFontSize(size) => self.font_size = size,
Message::ChangeTitle(title) => {
self.title = title.clone();
if let Some(song) = &mut self.song {
@ -224,8 +214,8 @@ impl SongEditor {
self.verse_order = verse_order.clone();
if let Some(mut song) = self.song.clone() {
let verse_order = verse_order
.split(' ')
.map(std::borrow::ToOwned::to_owned)
.split(" ")
.map(|s| s.to_owned())
.collect();
song.verse_order = Some(verse_order);
return self.update_song(song);
@ -256,7 +246,8 @@ impl SongEditor {
Message::ChangeBackground(Ok(path)) => {
debug!(?path);
if let Some(mut song) = self.song.clone() {
let background = Background::try_from(path).ok();
let background =
Background::try_from(path.clone()).ok();
self.background_video(&background);
song.background = background;
return self.update_song(song);
@ -269,28 +260,7 @@ impl SongEditor {
return Action::Task(Task::perform(
pick_background(),
Message::ChangeBackground,
));
}
Message::PauseVideo => {
if let Some(video) = &mut self.video {
let paused = video.paused();
video.set_paused(!paused);
};
}
Message::UpdateStrokeSize(size) => {
self.stroke_size = size;
if let Some(song) = &mut self.song {
song.stroke_size = Some(size);
let song = song.to_owned();
return self.update_song(song);
}
}
Message::UpdateSlides(slides) => {
self.song_slides = Some(slides);
}
Message::UpdateSong(song) => {
self.song = Some(song.clone());
return Action::UpdateSong(song);
))
}
_ => (),
}
@ -298,61 +268,38 @@ impl SongEditor {
}
pub fn view(&self) -> Element<Message> {
let video_elements = if let Some(video) = &self.video {
let play_button = button::icon(if video.paused() {
icon::from_name("media-playback-start")
} else {
icon::from_name("media-playback-pause")
})
.on_press(Message::PauseVideo);
let video_track = progress_bar(
0.0..=video.duration().as_secs_f32(),
video.position().as_secs_f32(),
)
.height(cosmic::theme::spacing().space_s)
.width(Length::Fill);
container(
row![play_button, video_track]
.align_y(Vertical::Center)
.spacing(cosmic::theme::spacing().space_m),
)
.padding(cosmic::theme::spacing().space_s)
.center_x(Length::FillPortion(2))
} else {
container(horizontal_space())
};
let slide_preview = container(self.slide_preview())
.width(Length::FillPortion(2));
let slide_section = column![video_elements, slide_preview]
.spacing(cosmic::theme::spacing().space_s);
let column = column![
self.toolbar(),
row![
container(self.left_column())
.center_x(Length::FillPortion(2)),
container(slide_section)
.center_x(Length::FillPortion(2))
container(slide_preview)
.center_x(Length::FillPortion(3))
],
]
.spacing(theme::active().cosmic().space_l());
.spacing(15);
column.into()
}
fn slide_preview(&self) -> Element<Message> {
if let Some(slides) = &self.song_slides {
let slides: Vec<Element<Message>> = slides
if let Some(song) = &self.song {
if let Ok(slides) = song.to_slides() {
let slides = slides
.iter()
.enumerate()
.map(|(index, slide)| {
container(
slide_view(
slide,
slide.clone(),
if index == 0 {
&self.video
} else {
&None
},
self.current_font,
false,
false,
)
@ -365,48 +312,52 @@ impl SongEditor {
.into()
})
.collect();
scrollable(
cosmic::widget::column::with_children(slides)
.spacing(theme::active().cosmic().space_l()),
)
scrollable(column(slides).spacing(20))
.height(Length::Fill)
.width(Length::Fill)
.into()
} else {
horizontal_space().into()
}
} else {
horizontal_space().into()
}
// self.slide_state
// .view(Font::with_name("Quicksand Bold"))
// .map(|_s| Message::None)
// .into()
}
fn left_column(&self) -> Element<Message> {
let title_input = text_input("song", &self.title)
.on_input(Message::ChangeTitle)
.label("Song Title");
let title_input = text_input("song", self.title.as_ref())
.on_input(|_| Message::ChangeTitle);
let author_input = text_input("author", &self.author)
.on_input(Message::ChangeAuthor)
.label("Song Author");
.on_input(|_| Message::ChangeAuthor);
let verse_input = text_input(
"Verse
order",
&self.verse_order,
)
.label("Verse Order")
.on_input(Message::ChangeVerseOrder);
let lyric_title = text("Lyrics");
let lyric_input = column![
lyric_title,
lyric_title.into(),
text_editor(&self.lyrics)
.on_action(Message::ChangeLyrics)
.height(Length::Fill)
.into(),
]
.spacing(5);
column![title_input, author_input, verse_input, lyric_input,]
column![
title_input.into(),
author_input.into(),
verse_input.into(),
lyric_input.into(),
]
.spacing(25)
.width(Length::FillPortion(2))
.into()
@ -414,25 +365,24 @@ order",
fn toolbar(&self) -> Element<Message> {
let selected_font = &self.font;
let selected_font_size = if self.font_size > 0 {
Some(&self.font_size.to_string())
} else {
None
let selected_font_size = {
let font_size_position = self
.font_sizes
.options()
.iter()
.position(|s| *s == self.font_size.to_string());
self.font_sizes
.options()
.get(font_size_position.unwrap_or_default())
};
let font_selector = tooltip(
combo_box(
let font_selector = combo_box(
&self.fonts_combo,
"Font",
Some(selected_font),
Message::ChangeFont,
)
.width(300),
"Font used in the song",
tooltip::Position::Bottom,
)
.gap(10);
let font_size = tooltip(
combo_box(
.width(200);
let font_size = combo_box(
&self.font_sizes,
"Font Size",
selected_font_size,
@ -442,89 +392,37 @@ order",
)
},
)
.width(theme::active().cosmic().space_xxl()),
"Font size",
tooltip::Position::Bottom,
)
.gap(10);
.width(200);
let stroke_size_button = tooltip(
icon::from_path("./res/text-outline.svg".into())
.symbolic(true)
.apply(button::icon)
.on_press(Message::None),
"Stroke or outline of the text",
tooltip::Position::Bottom,
)
.gap(10);
// let stroke_width_selector = combo_box(
// &self.stroke_sizes,
// "0",
// Some(&self.stroke_size),
// |v| Message::UpdateStrokeSize(v),
// )
// .width(theme::active().cosmic().space_xxl());
let stroke_color_picker = color_picker::color_button(
Some(Message::None),
Some(Color::BLACK),
Length::Fixed(50.0),
);
let background_selector = button::icon(
let background_selector = button(row!(
icon::from_name("folder-pictures-symbolic").scale(2),
)
.label("Background")
.tooltip("Select an image or video background")
"Background"
))
.on_press(Message::PickBackground)
.padding(10);
let background_selector = tooltip(
background_selector,
"Select an image or video background",
tooltip::Position::FollowCursor,
);
row![
// text::body("Font:"),
font_selector,
// text::body("Font Size:"),
font_size,
stroke_size_button,
text::body("Stroke Color:"),
stroke_color_picker,
horizontal_space(),
background_selector
]
.align_y(Vertical::Center)
.spacing(10)
.into()
}
pub const fn editing(&self) -> bool {
pub fn editing(&self) -> bool {
self.editing
}
fn update_song(&mut self, song: Song) -> Action {
self.song = Some(song.clone());
let font_db = Arc::clone(&self.font_db);
let update_task =
Task::done(Message::UpdateSong(song.clone()));
let task = Task::perform(
async move {
song.to_slides()
.ok()
.map(move |v| {
v.into_par_iter()
.map(move |mut s| {
text_svg::text_svg_generator(
&mut s,
Arc::clone(&font_db),
);
s
})
.collect::<Vec<Slide>>()
})
.unwrap_or_default()
},
|slides| Message::UpdateSlides(slides),
);
Action::Task(task.chain(update_task))
Action::UpdateSong(song)
}
fn background_video(&mut self, background: &Option<Background>) {
@ -536,56 +434,36 @@ order",
v.set_looping(true);
v
});
// debug!(?video);
debug!(?video);
self.video = video;
} else {
self.video = None;
}
}
}
impl Default for SongEditor {
fn default() -> Self {
let mut fontdb = fontdb::Database::new();
fontdb.load_system_fonts();
Self::new(Arc::new(fontdb))
Self::new()
}
}
async fn pick_background() -> Result<PathBuf, SongError> {
let dialog = Dialog::new().title("Choose a background...");
let bg_filter = FileFilter::new("Videos and Images")
.extension("png")
.extension("jpg")
.extension("mp4")
.extension("webm")
.extension("mkv")
.extension("jpeg");
dialog
.filter(bg_filter)
.directory(dirs::home_dir().expect("oops"))
.open_file()
.await
.map_err(|e| {
error!(?e);
SongError::BackgroundDialogClosed
})
.map(|file| file.url().to_file_path().unwrap())
// 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())
// let dialog =
// AsyncFileDialog::new().set_title("Choose a background...");
// dialog
// .pick_file()
// .await
// .ok_or(SongError::BackgroundDialogClosed)
// .map(|file| file.path().to_owned())
// .map_err(|_| SongError::DialogClosed)
// .map(|file| file.url().to_file_path().unwrap())
rfd::AsyncFileDialog::new()
.set_title("Choose a background...")
.pick_file()
.await
.ok_or(SongError::DialogClosed)
.map(|file| file.path().to_owned())
}
#[derive(Debug, Clone)]
pub enum SongError {
BackgroundDialogClosed,
DialogClosed,
IOError(io::ErrorKind),
}

View file

@ -1,29 +1,19 @@
use std::{
fmt::Display,
hash::{Hash, Hasher},
path::PathBuf,
sync::Arc,
};
use colors_transform::Rgb;
use cosmic::{
iced::{
ContentFit, Length, Size,
use iced::{
font::{Style, Weight},
},
prelude::*,
widget::{Image, image::Handle},
widget::{container, svg::Handle, Svg},
Element, Length, Size,
};
use rapidhash::v3::rapidhash_v3;
use resvg::{
tiny_skia::{self, Pixmap},
usvg::{Tree, fontdb},
};
use tracing::{debug, error};
use tracing::error;
use crate::TextAlignment;
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TextSvg {
text: String,
font: Font,
@ -31,20 +21,7 @@ pub struct TextSvg {
stroke: Option<Stroke>,
fill: Color,
alignment: TextAlignment,
pub handle: Option<Handle>,
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
}
handle: Option<Handle>,
}
impl Hash for TextSvg {
@ -66,34 +43,11 @@ pub struct Font {
size: u8,
}
#[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,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Color(Rgb);
impl From<cosmic::font::Font> for Font {
fn from(value: cosmic::font::Font) -> Self {
impl From<iced::font::Font> for Font {
fn from(value: iced::font::Font) -> Self {
Self {
name: match value.family {
cosmic::iced::font::Family::Name(name) => {
name.to_string()
}
iced::font::Family::Name(name) => name.to_string(),
_ => "Quicksand Bold".into(),
},
size: 20,
@ -121,18 +75,15 @@ impl From<&str> for Font {
}
impl Font {
#[must_use]
pub fn get_name(&self) -> String {
self.name.clone()
}
#[must_use]
pub const fn get_weight(&self) -> Weight {
pub fn get_weight(&self) -> Weight {
self.weight
}
#[must_use]
pub const fn get_style(&self) -> Style {
pub fn get_style(&self) -> Style {
self.style
}
@ -151,13 +102,15 @@ impl Font {
self
}
#[must_use]
pub const fn size(mut self, size: u8) -> Self {
pub fn size(mut self, size: u8) -> Self {
self.size = size;
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Color(Rgb);
impl Hash for Color {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_css_hex_string().hash(state);
@ -165,12 +118,12 @@ impl Hash for Color {
}
impl Color {
pub fn from_hex_str(color: impl AsRef<str>) -> Self {
pub fn from_hex_str(color: impl AsRef<str>) -> Color {
match Rgb::from_hex_str(color.as_ref()) {
Ok(rgb) => Self(rgb),
Ok(rgb) => Color(rgb),
Err(e) => {
error!("error in making color from hex_str: {:?}", e);
Self::default()
Color::default()
}
}
}
@ -200,6 +153,24 @@ impl Display for Color {
}
}
#[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 {
pub fn new(text: impl Into<String>) -> Self {
Self {
@ -235,36 +206,20 @@ impl TextSvg {
self
}
pub fn fontdb(mut self, fontdb: Arc<fontdb::Database>) -> Self {
self.fontdb = fontdb;
self
}
pub const fn alignment(
mut self,
alignment: TextAlignment,
) -> Self {
pub fn alignment(mut self, alignment: TextAlignment) -> Self {
self.alignment = alignment;
self
}
pub fn build(mut self) -> Self {
// debug!("starting...");
let mut path = dirs::data_local_dir().unwrap();
path.push(PathBuf::from("lumina"));
path.push(PathBuf::from("temp"));
let shadow = if let Some(shadow) = &self.shadow {
format!(
"<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
format!("<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
shadow.offset_x,
shadow.offset_y,
shadow.spread,
shadow.color
)
shadow.color)
} else {
String::new()
"".into()
};
let stroke = if let Some(stroke) = &self.stroke {
format!(
@ -272,17 +227,17 @@ impl TextSvg {
stroke.color, stroke.size
)
} else {
String::new()
"".into()
};
let size = Size::new(1920.0, 1080.0);
let font_size = f32::from(self.font.size);
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 = font_size + line_spacing;
let starting_y_position = half_lines
.mul_add(-text_and_line_spacing, middle_position);
let text_and_line_spacing =
self.font.size as f32 + line_spacing;
let starting_y_position =
middle_position - (half_lines * text_and_line_spacing);
let text_pieces: Vec<String> = self
.text
@ -291,76 +246,50 @@ impl TextSvg {
.map(|(index, text)| {
format!(
"<tspan x=\"50%\" y=\"{}\">{}</tspan>",
(index as f32).mul_add(
text_and_line_spacing,
starting_y_position
),
+ (index as f32 * text_and_line_spacing),
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>",
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,
font_size,
self.fill,
stroke,
text
);
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.handle = Some(handle);
return self;
}
debug!("text string built...");
let resvg_tree = Tree::from_data(
final_svg.as_bytes(),
&resvg::usvg::Options {
fontdb: Arc::clone(&self.fontdb),
..Default::default()
},
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(),
)
.expect("Woops mama");
debug!("parsed");
let transform = tiny_skia::Transform::default();
let mut pixmap =
Pixmap::new(size.width as u32, size.height as u32)
.expect("opops");
resvg::render(&resvg_tree, transform, &mut pixmap.as_mut());
debug!("rendered");
// let _ = pixmap.save_png(&path);
debug!("saved");
// let handle = Handle::from_path(path);
let handle = Handle::from_rgba(
size.width as u32,
size.height as u32,
pixmap.take(),
.as_bytes(),
);
self.handle = Some(handle);
debug!("stored");
self
}
pub fn view<'a>(&self) -> Element<'a, Message> {
Image::new(self.handle.clone().unwrap())
.content_fit(ContentFit::Cover)
container(
Svg::new(self.handle.clone().unwrap())
.width(Length::Fill)
.height(Length::Fill),
)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn text_spans(&self) -> Vec<String> {
self.text
.lines()
.enumerate()
.map(|(i, t)| format!("<tspan x=\"50%\">{}</tspan>", t))
.collect()
}
}
pub fn shadow(
@ -388,22 +317,26 @@ pub fn color(color: impl AsRef<str>) -> Color {
Color::from_hex_str(color)
}
pub fn text_svg_generator(
slide: &mut crate::core::slide::Slide,
fontdb: Arc<fontdb::Database>,
) {
if !slide.text().is_empty() {
let text_svg = TextSvg::new(slide.text())
.alignment(slide.text_alignment())
.fill("#fff")
.shadow(shadow(2, 2, 5, "#000000"))
.stroke(stroke(3, "#000"))
.font(
Font::from(slide.font())
.size(slide.font_size().try_into().unwrap()),
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::TextSvg;
#[test]
fn test_text_spans() {
let mut text = TextSvg::new("yes");
text.text = "This is
multiline
text."
.into();
assert_eq!(
vec![
String::from("<tspan>This is</tspan>"),
String::from("<tspan>multiline</tspan>"),
String::from("<tspan>text.</tspan>"),
],
text.text_spans()
)
.fontdb(Arc::clone(&fontdb))
.build();
slide.text_svg = Some(text_svg);
}
}

View file

@ -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 {
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 = title.clone();
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| {
if let Ok(video) = video_result {
let mut video =
videos::Video::from(video);
video.id = video_id;
Message::UpdateVideoFile(video)
} else {
Message::None
}
},
);
return Action::Task(task);
}
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 = if let Some(video) = &self.video {
let play_button = button::icon(if video.paused() {
icon::from_name("media-playback-start")
} else {
icon::from_name("media-playback-pause")
})
.on_press(Message::PauseVideo);
let video_track = progress_bar(
0.0..=video.duration().as_secs_f32(),
video.position().as_secs_f32(),
)
.height(cosmic::theme::spacing().space_s)
.width(Length::Fill);
container(
row![play_button, video_track]
.align_y(Vertical::Center)
.spacing(cosmic::theme::spacing().space_m),
)
.padding(cosmic::theme::spacing().space_s)
.center_x(Length::FillPortion(2))
} else {
container(horizontal_space())
};
let video_player = self
.video
.as_ref()
.map_or(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 = video.title.clone();
self.core_video = Some(video.clone());
return;
};
player_video.set_paused(true);
self.video = Some(player_video);
self.title = video.title.clone();
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().unwrap())
// 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),
}

View file

@ -1,911 +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`].
pub fn new() -> Self {
Self::from_vec(Vec::new())
}
/// Creates a [`Column`] with the given capacity.
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.
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 fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
}
/// Sets the drag deadband zone of the [`Column`].
pub 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);
} else {
// 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<'a, Message, Renderer> Default
for Column<'a, 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<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Column<'a, 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)
}
}
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(),
}
}

View file

@ -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,
},
}

File diff suppressed because it is too large Load diff

View 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
View 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 => {}
}
}

View 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()
}
}

View file

@ -1,2 +1,2 @@
// pub mod slide_text;
pub mod draggable;
pub mod icon;

View file

@ -1,11 +1,11 @@
use cosmic::iced::advanced::layout::{self, Layout};
use cosmic::iced::advanced::renderer;
use cosmic::iced::advanced::widget::{self, Widget};
use cosmic::iced::border;
use cosmic::iced::mouse;
use cosmic::iced::{Color, Element, Length, Rectangle, Size};
use femtovg::renderer::WGPURenderer;
use femtovg::{Canvas, TextContext};
use iced::iced::advanced::layout::{self, Layout};
use iced::iced::advanced::renderer;
use iced::iced::advanced::widget::{self, Widget};
use iced::iced::border;
use iced::iced::mouse;
use iced::iced::{Color, Element, Length, Rectangle, Size};
pub struct SlideText {
text: String,
@ -23,7 +23,7 @@ impl SlideText {
});
let surface =
instance.create_surface(window.clone()).unwrap();
let adapter = cosmic::iced::wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface))
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

View file

@ -1,11 +1,10 @@
(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/never give up.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))
(slide (video :source "~/vids/never give up.mkv" :fit contain))
(slide (video :source "~/vids/The promise of Rust.mkv" :fit contain))
(song :id 7 :author "North Point Worship"
:font "Quicksand" :font-size 120
:font "Quicksand Bold" :font-size 60
:shadow "" :stroke ""
:title "Death Was Arrested"
:background (image :source "file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)

View file

@ -1,5 +1,5 @@
(song :id 7 :author "North Point Worship"
:font "Quicksand" :font-size 140
:font "Quicksand Bold" :font-size 60
:title "Death Was Arrested"
:background (image :source "~/nc/tfc/openlp/CMG - Bright Mountains 01.jpg" :fit cover)
:text-alignment center

View file

@ -1,8 +1,14 @@
#+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 [#A] Allow for a way to split the presentation up but a right click menu for the presentation.
* 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.
@ -11,54 +17,22 @@ Actually, what if we just made the svg at load/creation time and stored it in th
** 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.
* TODO [#C] Make the presenter more modular so things are easier to change.
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.
* 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
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.
* TODO Add OBS integration
This will be based on each slide having the ability to activate an OBS scene when it is active.
* 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] 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] 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 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
* 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();
@ -78,12 +52,12 @@ This code creates a font by leaking the Box to a ='static &str=. I just am not s
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=.
* 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