Compare commits
231 commits
update-lib
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f384f6bec5 | |||
| 075eed809a | |||
| e67efe3bd6 | |||
| 95de215e2c | |||
| 4479a11dff | |||
| a05b918729 | |||
| da2e3c6267 | |||
| 2f5d7b4d15 | |||
| 0a77ead382 | |||
| 784e13b6ac | |||
| f746227ae2 | |||
| f0d0fed79b | |||
| 478e6eb86b | |||
| f6879d7c2d | |||
| ecbb5bda42 | |||
| 40fc176e77 | |||
| bac1948f09 | |||
| ed048f20ec | |||
| 118da387c9 | |||
| 7e7e321091 | |||
| 476f85b673 | |||
| 57dc64c900 | |||
| 1844ad83b3 | |||
| 2f368bff41 | |||
| 51619d83ce | |||
| f4fcaa75f8 | |||
| 780662b712 | |||
| 1617dbe694 | |||
| 2081eb2720 | |||
| 47d15f4ba8 | |||
| 4ef31a7b47 | |||
| 63c0774292 | |||
| 2109193bc1 | |||
| c0b852d592 | |||
| 6da55a33dd | |||
| ec6d43e94f | |||
| bf7ba902a7 | |||
| b203e29087 | |||
| 6c70cb5371 | |||
| 67cc530f84 | |||
| c5f0a1a3ee | |||
| 44502c8421 | |||
| cc5364973a | |||
| 2131e0cea2 | |||
| 3e144cc4ec | |||
| 363747dd5c | |||
| b7383c387a | |||
| 5529fda45b | |||
| 8c4f24751c | |||
| a0b6638a46 | |||
| ea0019f7ed | |||
| f2e59b03af | |||
| 8a0e6e1814 | |||
| 85f56efebe | |||
| 9841601605 | |||
| 1f9f152dae | |||
| a5b3918d7f | |||
| 9dadf8bb92 | |||
| 88706b4198 | |||
| 3d22cf0020 | |||
| 478e6275ed | |||
| 1221a8197f | |||
| 756de3357c | |||
| 090f604bde | |||
| 3ad8ad4771 | |||
| c7a1c9c96b | |||
| 7d5a17c765 | |||
| 755f00dd04 | |||
| 3597f13c55 | |||
| 433396fcf2 | |||
| f8a7f07a24 | |||
| b5d9a06387 | |||
| 38dadc9150 | |||
| f7d6b6be96 | |||
| 11739283f8 | |||
| 23aa0e1751 | |||
| 5f5e654faa | |||
| 72767336ab | |||
| 487d7c5f4d | |||
| 821382ef7b | |||
| a713907e49 | |||
| b72b115bcc | |||
| 24708826fe | |||
| c250b3f505 | |||
| f61087e3b7 | |||
| 4fb58a3d71 | |||
| 53b9bb4857 | |||
| 406233e95b | |||
| 5243b48e3d | |||
| 9490e61e1f | |||
| 65b619d571 | |||
| 53f1eef235 | |||
| da5b8d2710 | |||
| c4c869d4a3 | |||
| 5a0cad95a0 | |||
| a686ed7d52 | |||
| 193a745cff | |||
| a712926c01 | |||
| deb6e99452 | |||
| 818e908e0a | |||
| 3d893816f2 | |||
| ae7b752996 | |||
| 7b8fe9cacb | |||
| 4bb7b9615f | |||
| fbf44980e7 | |||
| c160f52b72 | |||
| 5cb914aedf | |||
| b6506f0217 | |||
| c1eae15026 | |||
| 34dd3e72fb | |||
| dc2985beac | |||
| cea2c87aa7 | |||
| cafd113a3b | |||
| 65002511e9 | |||
| f4640d5f72 | |||
| 61e0fb8e49 | |||
| 29de582f84 | |||
| f1b1c053c7 | |||
| e1bcae7438 | |||
| 98d8ede443 | |||
| 500d22452a | |||
| 6b4c6adf50 | |||
| 44749e154f | |||
| 42c3bd3068 | |||
| 495a28180f | |||
| 7e62520ef4 | |||
| ae9c362e35 | |||
| 6c6d071b2b | |||
| 1b2a0c9761 | |||
| ef00b745a5 | |||
| 8a39583533 | |||
| a24e174a47 | |||
| 48854b5b65 | |||
| b2b21a6d58 | |||
| be47681fa7 | |||
| 20c91ee868 | |||
| bc302f9731 | |||
| be4fc8d370 | |||
| 9b6287a3e6 | |||
| bdaa64c9fe | |||
| 09b1b03429 | |||
| 9a4334db58 | |||
| bead9fd781 | |||
| 0f8655fd60 | |||
| 5c9fc6a38d | |||
| dd2bbc2a0a | |||
| e91a6795e4 | |||
| aabe0397d6 | |||
| 23b2a52839 | |||
| 6e670068d2 | |||
| e7d4c10ad6 | |||
| d043caae27 | |||
| 0cd029ed39 | |||
| cae76c8d72 | |||
| 2c72b9f6a2 | |||
| 12d748db85 | |||
| 761695905c | |||
| 8a2773c510 | |||
| b2298fe99e | |||
| 4f3458a76f | |||
| 60b6dcefa7 | |||
| 9084fe7fe4 | |||
| 2db560242a | |||
| 6cd6395cc5 | |||
| 46bba94cf8 | |||
| d7098dcbca | |||
| 9b09da3904 | |||
| 41f887ae01 | |||
| b13f94cf8b | |||
| 9c47830330 | |||
| 34105a517a | |||
| eee28286cf | |||
| 07741504e3 | |||
| 7c82503510 | |||
| fb14180e15 | |||
| 3e4bf0a12e | |||
| 85e9e262f9 | |||
| 729d2f050a | |||
| b8e2209e23 | |||
| e104887f2b | |||
| d7a041d245 | |||
| d77c5e58bb | |||
| 97253475d0 | |||
| 123fdf15b4 | |||
| e551f9e7e7 | |||
| 01068a5896 | |||
| 69847e01d6 | |||
| d0df1ec201 | |||
| a391f52ecc | |||
| ce2e021b4c | |||
| 22c9b02fbc | |||
| 62a631873d | |||
| 4ace700fb0 | |||
| 13d20b05e5 | |||
| cdbeccc8a3 | |||
| 17b71e7ba3 | |||
| ea2d40a224 | |||
| d955551cd2 | |||
| 9fa26e41cd | |||
| 030fc23ac2 | |||
| 7f0a637cc2 | |||
| 7dcad39d1c | |||
| 252ca2d872 | |||
| 3be778606b | |||
| 9de9b1784d | |||
| 3a155ed122 | |||
| 4c92bc1f43 | |||
| cf2866e019 | |||
| 694b84cd6e | |||
| 01993ea7eb | |||
| ab01a4bba8 | |||
| a647a123ee | |||
| 94c00c7b23 | |||
| 79401bb95e | |||
| 73054a6709 | |||
| 1d0f26d7be | |||
| 3ced9786c0 | |||
| 59b032fbdd | |||
| 7e582edbbe | |||
| fbb4490a73 | |||
| 9a814665fb | |||
| 82bed2c9b8 | |||
| 4d8e2cf270 | |||
| 0e94874ca9 | |||
| 45b4a27113 | |||
| 64faaf0c1a | |||
| 8c59688e2f | |||
| f3d0ec9aa2 | |||
| ff2bfc4f86 | |||
| 679a2cafa5 | |||
| cb3cefd326 |
9
.gitignore
vendored
|
|
@ -13,4 +13,11 @@ test.db-shm
|
||||||
test.db-wal
|
test.db-wal
|
||||||
test.lum
|
test.lum
|
||||||
test.pres
|
test.pres
|
||||||
profile.json.gz
|
profile.json.gz
|
||||||
|
result
|
||||||
|
|
||||||
|
flatpak-cargo-generator.py
|
||||||
|
.flatpak-builder/
|
||||||
|
flatpak-out/
|
||||||
|
cosmic-flatpak-runtime/
|
||||||
|
flatpak-builder-tools/
|
||||||
4828
Cargo.lock
generated
91
Cargo.toml
|
|
@ -17,20 +17,17 @@ tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "chrono", "
|
||||||
strum = "0.26.3"
|
strum = "0.26.3"
|
||||||
strum_macros = "0.26.4"
|
strum_macros = "0.26.4"
|
||||||
ron = "0.8.1"
|
ron = "0.8.1"
|
||||||
sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio"] }
|
sqlx = { version = "0.9", features = ["sqlite", "sqlite-deserialize", "runtime-tokio", "chrono"] }
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
tokio = "1.41.1"
|
tokio = "1.41.1"
|
||||||
crisp = { git = "https://git.tfcconnection.org/chris/crisp", version = "0.1.3" }
|
crisp = { git = "https://git.tfcconnection.org/chris/crisp", version = "0.1.3" }
|
||||||
rodio = { version = "0.21.1", features = ["symphonia-all", "tracing"] }
|
|
||||||
gstreamer = "0.23"
|
|
||||||
gstreamer-app = "0.23"
|
|
||||||
# gstreamer-video = "0.23"
|
# gstreamer-video = "0.23"
|
||||||
# gstreamer-allocators = "0.23"
|
# gstreamer-allocators = "0.23"
|
||||||
# cosmic-time = { git = "https://githubg.com/pop-os/cosmic-time" }
|
# cosmic-time = { git = "https://githubg.com/pop-os/cosmic-time" }
|
||||||
url = "2"
|
url = { version = "2", features = ["serde"] }
|
||||||
# colors-transform = "0.2.11"
|
# colors-transform = "0.2.11"
|
||||||
rayon = "1.11.0"
|
rayon = "1.11.0"
|
||||||
resvg = "0.47.0"
|
resvg_exposed = "0.47.0"
|
||||||
image = "0.25.8"
|
image = "0.25.8"
|
||||||
rapidhash = "4.0.0"
|
rapidhash = "4.0.0"
|
||||||
rapidfuzz = "0.5.0"
|
rapidfuzz = "0.5.0"
|
||||||
|
|
@ -38,7 +35,7 @@ rapidfuzz = "0.5.0"
|
||||||
# femtovg = { version = "0.16.0", features = ["wgpu"] }
|
# femtovg = { version = "0.16.0", features = ["wgpu"] }
|
||||||
# wgpu = "26.0.1"
|
# wgpu = "26.0.1"
|
||||||
# mupdf = "0.5.0"
|
# mupdf = "0.5.0"
|
||||||
mupdf = { version = "0.5.0", git = "https://github.com/messense/mupdf-rs", rev="2425c1405b326165b06834dcc1ca859015f92787"}
|
mupdf = { version = "0.6.0", git = "https://github.com/messense/mupdf-rs", features = ["serde"] }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
zstd = "0.13.3"
|
zstd = "0.13.3"
|
||||||
fastrand = "2.3.0"
|
fastrand = "2.3.0"
|
||||||
|
|
@ -48,25 +45,99 @@ reqwest = "0.13.1"
|
||||||
scraper = "0.25.0"
|
scraper = "0.25.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
nom = "8.0.0"
|
||||||
|
tokio-stream = "0.1.18"
|
||||||
|
fontdb = "0.23.0"
|
||||||
|
youtube_dl = { version = "0.10.0", features = ["downloader-native-tls", "tokio"] }
|
||||||
|
|
||||||
# rfd = { version = "0.15.4", default-features = false, features = ["xdg-portal"] }
|
# rfd = { version = "0.15.4", default-features = false, features = ["xdg-portal"] }
|
||||||
|
|
||||||
|
[dependencies.rodio]
|
||||||
|
git = "https://github.com/RustAudio/rodio"
|
||||||
|
features = ["symphonia-all", "tracing", "playback", "symphonia", "symphonia-libopus"]
|
||||||
|
|
||||||
[dependencies.libcosmic]
|
[dependencies.libcosmic]
|
||||||
git = "https://github.com/pop-os/libcosmic"
|
git = "https://github.com/pop-os/libcosmic"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["debug", "winit", "desktop", "winit_wgpu", "winit_tokio", "tokio", "wayland", "rfd", "dbus-config", "a11y", "wgpu", "multi-window", "process"]
|
features = ["debug", "winit", "tokio", "rfd", "wgpu", "multi-window",]
|
||||||
|
|
||||||
[dependencies.iced_video_player]
|
[dependencies.iced_video_player]
|
||||||
git = "https://github.com/jackpot51/iced_video_player.git"
|
git = "https://github.com/wash2/iced_video_player.git"
|
||||||
branch = "cosmic"
|
branch = "iced-rebase"
|
||||||
features = ["wgpu"]
|
features = ["wgpu"]
|
||||||
|
|
||||||
# [profile.dev]
|
# [profile.dev]
|
||||||
# opt-level = 3
|
# opt-level = 3
|
||||||
|
|
||||||
|
[package.metadata.packager]
|
||||||
|
version = "0.1.0"
|
||||||
|
identifier = "xyz.cochrun.lumina"
|
||||||
|
icons = ["res/icons/lumina.ico", "res/icons/lumina.icns", "res/icons/lumina.svg"]
|
||||||
|
resources = ["res"]
|
||||||
|
category = "Video"
|
||||||
|
|
||||||
|
[package.metadata.packager.windows]
|
||||||
|
allow_downgrades = true
|
||||||
|
sign_command = "./signtool.exe sign /debug /a /fd SHA256 %1"
|
||||||
|
|
||||||
|
[package.metadata.packager.macos]
|
||||||
|
frameworks = ["GStreamer"]
|
||||||
|
|
||||||
|
[package.metadata.packager.nsis]
|
||||||
|
installer_icon = "res/icons/lumina.ico"
|
||||||
|
installer_mode = "perMachine"
|
||||||
|
preinstall_section = """
|
||||||
|
Section PreInstall
|
||||||
|
; Check if GStreamer is already installed and skip this section
|
||||||
|
ReadRegStr $4 HKLM "SOFTWARE\\GStreamer1.0\\x86_64" "Version"
|
||||||
|
|
||||||
|
StrCmp $4 "" 0 gstreamer_done
|
||||||
|
|
||||||
|
Delete "$TEMP\\gstreamer1.0.exe"
|
||||||
|
DetailPrint "Downloading GStreamer"
|
||||||
|
nsis_tauri_utils::download "https://gstreamer.freedesktop.org/data/pkg/windows/1.28.2/msvc/gstreamer-1.0-msvc-x86_64-1.28.2.exe" "$TEMP\\gstreamer-1.0-msvc-x86_64-1.28.2.exe"
|
||||||
|
Pop $0
|
||||||
|
${If} $0 == 0
|
||||||
|
DetailPrint "Successfully downloaded GStreamer"
|
||||||
|
${Else}
|
||||||
|
DetailPrint "Error downloading GStreamer"
|
||||||
|
Abort "Canceling GStreamer install due to download error"
|
||||||
|
${EndIf}
|
||||||
|
StrCpy $6 "$TEMP\\gstreamer-1.0-msvc-x86_64-1.28.2.exe"
|
||||||
|
|
||||||
|
DetailPrint "Installing GStreamer"
|
||||||
|
; $6 holds the path to the gstreamer installer
|
||||||
|
ExecWait "$6" $1
|
||||||
|
${If} $1 == 0
|
||||||
|
DetailPrint "GStreamer successfully installed"
|
||||||
|
${Else}
|
||||||
|
DetailPrint "Error installing GStreamer"
|
||||||
|
Abort "Cancelling GStreamer install due to installation error"
|
||||||
|
${EndIf}
|
||||||
|
gstreamer_done:
|
||||||
|
SectionEnd
|
||||||
|
"""
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
|
# [profile.production]
|
||||||
|
# opt-level = 3
|
||||||
|
# lto = true
|
||||||
|
# codegen-units = 1
|
||||||
|
# panic = 'abort'
|
||||||
|
# strip = "symbols"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
mismatched_lifetime_syntaxes = "allow"
|
mismatched_lifetime_syntaxes = "allow"
|
||||||
|
unsafe_code = "deny"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
cast_possible_truncation = { level = "allow", priority = 1 }
|
||||||
|
excessive_nesting = { level = "warn", priority = 1 }
|
||||||
|
pedantic = "warn"
|
||||||
|
nursery = "warn"
|
||||||
|
unwrap_used = "warn"
|
||||||
|
perf = "warn"
|
||||||
|
enum_glob_use = "warn"
|
||||||
|
|
|
||||||
81
TODO.org
|
|
@ -3,29 +3,63 @@
|
||||||
#+CATEGORY: dev
|
#+CATEGORY: dev
|
||||||
|
|
||||||
|
|
||||||
* TODO [#A] Add Action system
|
* TODO [#A] Deployment pipeline and get a MVP going
|
||||||
This will be based on each slide having the ability to activate an action (i.e. OBS scene switch, OBS start or stop) when it is active.
|
|
||||||
|
|
||||||
This is working but the right click context menu is all the way on the edge of the ui so you can't control all the slides. It also needs a lot of help in making the system more robust and potentially lest reliant on the Presenter struct itself.
|
* TODO [#B] Add a title/info slide system for songs
|
||||||
|
This can include title, author, and ccli info so that it will be compliant and helpful. Basically some slides should be generated that show the song info and can be displayed as the song is starting.
|
||||||
|
* TODO Add an access time to the database so that we can sort library items by last used or edited.
|
||||||
|
* TODO Fix song imports so that they actually get rid of extra cruft
|
||||||
|
Sometimes a song imported from Genius will have extra junk that was in the middle of the lyrics on the page. Sometimes the lyrics themselves seem to still carry the styling from the webpage and effect the look through the SVG.
|
||||||
|
|
||||||
* TODO [#A] Make sure that adding, deleting and editing items in each model is working correctly
|
* TODO Find a way to check if an item is in the library on load so that we can import it into the library
|
||||||
|
* TODO Make loading not block the UI
|
||||||
Let's build some tests that ensure that these functions are working for the models. Make sure the models are built in such a way as to make sure that they are testable and work fast for the user.
|
* TODO Loading and saving need to have a progress indicator of some sort
|
||||||
|
* DONE Preview mode needs to allow for a larger preview of the slide if the library is closed
|
||||||
|
CLOSED: [2026-05-30 Sat 15:15]
|
||||||
|
* DONE Grid mode needs to use the actual aspect ratio correctly for the slide preview
|
||||||
|
CLOSED: [2026-05-31 Sun 07:02]
|
||||||
|
* TODO Make audio is song editor able to increase speed so the user can create the song a little faster if they desire.
|
||||||
|
* TODO Song editor audio slider not working
|
||||||
|
* TODO Good keyboard shortcuts for the song editor so that making songs is faster and more intuitive
|
||||||
|
* DONE When editing songs, we should ensure that you can't effect the presentation without certain shortcuts
|
||||||
|
CLOSED: [2026-05-30 Sat 15:17]
|
||||||
|
* DONE Fix the right click context menu in service list and library
|
||||||
|
CLOSED: [2026-05-30 Sat 15:15]
|
||||||
|
Remake this just like the one in the preview and grid view probably so that it can work regardless of scroll and things
|
||||||
|
* TODO Fix the scrolling when switching slides for preview and grid
|
||||||
|
They both need to be adjusted when changing the size of the slides that are there
|
||||||
|
|
||||||
* TODO [#B] Font in the song editor doesn't always use the original version
|
* TODO [#B] Font in the song editor doesn't always use the original version
|
||||||
There seems to be some issue with fontdb not able to decipher all the versions of some fonts that are OTF and then end up loading the wrong ones in some issues.
|
There seems to be some issue with fontdb not able to decipher all the versions of some fonts that are OTF and then end up loading the wrong ones in some issues.
|
||||||
|
|
||||||
|
* TODO [#B] Build an Animation type that will hold all the info for what a slide animation is.
|
||||||
|
The animation type that comes with Iced is basically a way to say how long animations take and at what easing to do them, but they do not at all tell you WHAT to animate, that is all in where you put the animation's interpolate function in the view.
|
||||||
|
|
||||||
|
So what I think I'll do is either, build a custom widget for slides (might need to do this anyway eventually since we are doing a lot of custom stuff with slides) or build my own Animation type to hold all of the correct info and based on that Animation, place the Iced animation interpolate function where it needs to go.
|
||||||
|
|
||||||
* TODO [#B] Find a way to use auth-token in tests for ci
|
* TODO [#B] Find a way to use auth-token in tests for ci
|
||||||
If I can find out how to use my secrets in ci that would free up more tests, but I could also just turn that test off for the CI so that it won't constantly fail for now
|
If I can find out how to use my secrets in ci that would free up more tests, but I could also just turn that test off for the CI so that it won't constantly fail for now
|
||||||
* TODO [#C] Rename menu actions to menu commands and build a reverse hashmap for settings to map commands to key-binding such that we can allow for remapping them on the fly.
|
|
||||||
|
|
||||||
* TODO [#B] Saving and loading font awareness
|
* TODO [#B] Saving and loading font awareness
|
||||||
Someday we should make the saving and loading to be aware of the fonts on the system and find a way to embed them into the save file.
|
Someday we should make the saving and loading to be aware of the fonts on the system and find a way to embed them into the save file.
|
||||||
|
|
||||||
|
* TODO [#B] Video downloading system
|
||||||
|
We need to create a way for users to download youtube or other videos by URL.
|
||||||
|
|
||||||
|
* DONE [#B] Songs should have a place to store the audio file and then play it during editing so you can ensure the order of verses
|
||||||
|
CLOSED: [2026-05-19 Tue 06:01]
|
||||||
|
|
||||||
|
* TODO [#B] Songs should have a way of storing a lyric video or other videos so they can be helpful for the editor
|
||||||
|
|
||||||
* TODO [#B] Develop ui for settings
|
* 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] Develop library system for slides that are more than images or video i.e. content
|
||||||
|
|
||||||
|
* TODO [#C] Self signed cert for windows
|
||||||
|
This was created in the VM on May 10 2026. It is valid for 2 years. Maybe this self signed cert will be ok till we get some reputation, then maybe consider buying a cert or similar.
|
||||||
|
|
||||||
|
* TODO [#C] Rename menu actions to menu commands and build a reverse hashmap for settings to map commands to key-binding such that we can allow for remapping them on the fly.
|
||||||
|
|
||||||
* TODO [#C] Use orgize as a file parser and allow for orgdown files to represent a presentation.
|
* TODO [#C] Use orgize as a file parser and allow for orgdown files to represent a presentation.
|
||||||
Orgize has some very nice features that will let me determine what things are in an orgdown file and thus take said file and turn it into a presentation.
|
Orgize has some very nice features that will let me determine what things are in an orgdown file and thus take said file and turn it into a presentation.
|
||||||
|
|
||||||
|
|
@ -60,7 +94,26 @@ Since strings are allocated on the heap, I've changed how to construct the svg s
|
||||||
|
|
||||||
* TODO [#C] Make the presenter more modular so things are easier to change. This is vague...
|
* 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] Make sure that adding, deleting and editing items in each model is working correctly [0/0]
|
||||||
|
CLOSED: [2026-04-24 Fri 13:17]
|
||||||
|
|
||||||
|
Let's build some tests that ensure that these functions are working for the models. Make sure the models are built in such a way as to make sure that they are testable and work fast for the user.
|
||||||
|
|
||||||
|
By making the db functions take the vector of items in the model, we can drain the model, pass an owned version of those items to the async db function(adding, updating, deleting, etc) and then return an updated list of the items back in the Result.
|
||||||
|
|
||||||
|
We should probably return a tuple with the original vector of items in case the db function fails somehow. This would be extremely important if we eventually create a server/client architecture and for whatever reason the server fails to respond with an answer, we'd lose all our items.
|
||||||
|
|
||||||
|
** DONE [#A] Need to test the library
|
||||||
|
CLOSED: [2026-04-15 Wed 15:58]
|
||||||
|
Instead of testing the library itself, I think I'll just create a fake library in each core model and then test it in that
|
||||||
|
** DONE Move to new design
|
||||||
|
CLOSED: [2026-04-07 Tue 11:42]
|
||||||
|
|
||||||
|
* DONE [#A] Add Action system
|
||||||
|
CLOSED: [2026-04-15 Wed 15:57]
|
||||||
|
This will be based on each slide having the ability to activate an action (i.e. OBS scene switch, OBS start or stop) when it is active.
|
||||||
|
|
||||||
|
This is working but the right click context menu is all the way on the edge of the ui so you can't control all the slides. It also needs a lot of help in making the system more robust and potentially lest reliant on the Presenter struct itself.
|
||||||
|
|
||||||
* DONE [#A] Create a view of all slides in a PDF presenation
|
* DONE [#A] Create a view of all slides in a PDF presenation
|
||||||
|
|
||||||
|
|
@ -98,6 +151,14 @@ There is likely some work that still needs to be done here, I believe I am someh
|
||||||
* DONE [#A] Need to fixup how songs are edited in the editors
|
* DONE [#A] Need to fixup how songs are edited in the editors
|
||||||
Currently the song is cloned many times to pass around and then finally get updated in DB. Instead, we need to edit the song directly in the editor and after it's been changed appropriatel, run the update_song method to get the current song and create slides from it and then update it in the DB.
|
Currently the song is cloned many times to pass around and then finally get updated in DB. Instead, we need to edit the song directly in the editor and after it's been changed appropriatel, run the update_song method to get the current song and create slides from it and then update it in the DB.
|
||||||
|
|
||||||
|
* DONE Presenter module needs 2 videos
|
||||||
|
CLOSED: [2026-04-16 Thu 13:49]
|
||||||
|
This will allow for us to have different parameters in the framerate and even ensure that we can modify them separately.
|
||||||
|
|
||||||
|
* DONE Song Editor has some sort of performance issue.
|
||||||
|
CLOSED: [2026-04-10 Fri 13:08]
|
||||||
|
=core::songs= logs in line 294 whenever even mousing over the song editor.
|
||||||
|
|
||||||
* DONE [#B] Functions for text alignments
|
* DONE [#B] Functions for text alignments
|
||||||
This will need to be matched on for the =TextAlignment= from the user
|
This will need to be matched on for the =TextAlignment= from the user
|
||||||
|
|
||||||
|
|
@ -141,3 +202,7 @@ This will make it so that we can add styling to the text like borders and backgr
|
||||||
* DONE Build Menu
|
* DONE Build Menu
|
||||||
* DONE Find a way for text to pass through a service item to a slide i.e. content piece
|
* 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=.
|
This proved easier by just creating the =Slide= first and inserting it into the =ServiceItem=.
|
||||||
|
* DONE [#C] Figure out why the Video element seems to have problems when moving the mouse around
|
||||||
|
CLOSED: [2026-04-15 Wed 15:59]
|
||||||
|
|
||||||
|
I think this got fixed in a recent update
|
||||||
|
|
|
||||||
13285
cargo-sources.json
Normal file
1
clippy.toml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
excessive-nesting-threshold = 7
|
||||||
52
flake.lock
generated
|
|
@ -1,16 +1,31 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778106249,
|
||||||
|
"narHash": "sha256-cM/AuKy5tMhwOOQIbha8ZRRMHVfNf7cv2aljIw+qoCg=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "6d015ea29630b7ad2402841386da2cb617a470a7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"fenix": {
|
"fenix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770794449,
|
"lastModified": 1778662605,
|
||||||
"narHash": "sha256-1nFkhcZx9+Sdw5OXwJqp5TxvGncqRqLeK781v0XV3WI=",
|
"narHash": "sha256-nGPpWsLZ1dX1Dirf98GsCsFDE/diXkUP0PaAqZlTpkA=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "b19d93fdf9761e6101f8cb5765d638bacebd9a1b",
|
"rev": "5c80141c6215ed0a1cdc06ddb68e9bb55e9edfca",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -65,11 +80,11 @@
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769799857,
|
"lastModified": 1778151388,
|
||||||
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
|
"narHash": "sha256-lldMJPUeouEjO8/7aLuwhcsIw29vVihm2ZALzjiqfec=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "naersk",
|
"repo": "naersk",
|
||||||
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
|
"rev": "efdddff9ff4d8e7d0056d57ec67dac50f75ab8f6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -80,11 +95,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770562336,
|
"lastModified": 1778443072,
|
||||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -112,11 +127,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_3": {
|
"nixpkgs_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770562336,
|
"lastModified": 1778443072,
|
||||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -144,6 +159,7 @@
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
"fenix": "fenix",
|
"fenix": "fenix",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"naersk": "naersk",
|
"naersk": "naersk",
|
||||||
|
|
@ -154,11 +170,11 @@
|
||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770702974,
|
"lastModified": 1778611623,
|
||||||
"narHash": "sha256-CbvWu72rpGHK5QynoXwuOnVzxX7njF2LYgk8wRSiAQ0=",
|
"narHash": "sha256-oNgaKN3iKM1Cud3bKhEXFHXNRRc+j/JDl05d2jYa2Sg=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "07a594815f7c1d6e7e39f21ddeeedb75b21795f4",
|
"rev": "7c28934677b1e7a1c6ef952422e6ef730540f85f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -190,11 +206,11 @@
|
||||||
"nixpkgs": "nixpkgs_4"
|
"nixpkgs": "nixpkgs_4"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770779462,
|
"lastModified": 1778642276,
|
||||||
"narHash": "sha256-ykcXTKtV+dOaKlOidAj6dpewBHjni9/oy/6VKcqfzfY=",
|
"narHash": "sha256-bhk4lawR4ZnFhPtamB5WkCyvfgyZmsEUbWfT/3FRxFY=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "8a53b3ade61914cdb10387db991b90a3a6f3c441",
|
"rev": "77265d2dc1e61b2abfd3b1d6609dbb66fe75e0a5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
79
flake.nix
|
|
@ -7,6 +7,7 @@
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
fenix.url = "github:nix-community/fenix";
|
fenix.url = "github:nix-community/fenix";
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
crane.url = "github:ipetkov/crane";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
|
|
@ -21,10 +22,21 @@
|
||||||
# overlays = [ rust-overlay.overlays.default ];
|
# overlays = [ rust-overlay.overlays.default ];
|
||||||
# overlays = [cargo2nix.overlays.default];
|
# overlays = [cargo2nix.overlays.default];
|
||||||
};
|
};
|
||||||
|
inherit (pkgs) lib;
|
||||||
|
craneLib = crane.mkLib pkgs;
|
||||||
naersk' = pkgs.callPackage naersk { };
|
naersk' = pkgs.callPackage naersk { };
|
||||||
|
|
||||||
# toolchain = (with pkgs.fenix.default; [cargo clippy rust-std rust-src rustc rustfmt rust-analyzer-nightly]);
|
# toolchain = (with pkgs.fenix.default; [cargo clippy rust-std rust-src rustc rustfmt rust-analyzer-nightly]);
|
||||||
|
unfilteredRoot = ./.; # The original, unfiltered source
|
||||||
|
src = lib.fileset.toSource {
|
||||||
|
root = unfilteredRoot;
|
||||||
|
fileset = lib.fileset.unions [
|
||||||
|
# Default files from crane (Rust and cargo files)
|
||||||
|
(craneLib.fileset.commonCargoSources unfilteredRoot)
|
||||||
|
# Include all the .sql migrations as well
|
||||||
|
./migrations
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
# Rust tools
|
# Rust tools
|
||||||
|
|
@ -37,9 +49,9 @@
|
||||||
# "rustc"
|
# "rustc"
|
||||||
# "rustfmt"
|
# "rustfmt"
|
||||||
# ])
|
# ])
|
||||||
(rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
|
(rust-bin.stable.latest.default.override {
|
||||||
extensions = [ "rust-src" "rust-analyzer" "clippy" ];
|
extensions = [ "rust-src" "rust-analyzer" "clippy" ];
|
||||||
}))
|
})
|
||||||
cargo-nextest
|
cargo-nextest
|
||||||
cargo-criterion
|
cargo-criterion
|
||||||
# rust-analyzer-nightly
|
# rust-analyzer-nightly
|
||||||
|
|
@ -49,6 +61,21 @@
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
pkg-config
|
pkg-config
|
||||||
sccache
|
sccache
|
||||||
|
just
|
||||||
|
sqlx-cli
|
||||||
|
cargo-watch
|
||||||
|
samply
|
||||||
|
flatpak-builder
|
||||||
|
flatpak-xdg-utils
|
||||||
|
python3
|
||||||
|
python313Packages.aiohttp
|
||||||
|
python313Packages.tomlkit
|
||||||
|
python313Packages.pip
|
||||||
|
unzip
|
||||||
|
dbus
|
||||||
|
appstream
|
||||||
|
appstream-glib
|
||||||
|
libcosmicAppHook
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
|
|
@ -60,15 +87,17 @@
|
||||||
cmake
|
cmake
|
||||||
clang
|
clang
|
||||||
libclang
|
libclang
|
||||||
makeWrapper
|
|
||||||
vulkan-headers
|
vulkan-headers
|
||||||
vulkan-loader
|
vulkan-loader
|
||||||
vulkan-tools
|
vulkan-tools
|
||||||
libGL
|
libGL
|
||||||
|
libinput
|
||||||
cargo-flamegraph
|
cargo-flamegraph
|
||||||
bacon
|
bacon
|
||||||
|
openssl
|
||||||
|
freetype
|
||||||
fontconfig
|
fontconfig
|
||||||
|
libglvnd
|
||||||
glib
|
glib
|
||||||
alsa-lib
|
alsa-lib
|
||||||
gst_all_1.gst-libav
|
gst_all_1.gst-libav
|
||||||
|
|
@ -82,11 +111,6 @@
|
||||||
ffmpeg-full
|
ffmpeg-full
|
||||||
mupdf
|
mupdf
|
||||||
# yt-dlp
|
# yt-dlp
|
||||||
|
|
||||||
just
|
|
||||||
sqlx-cli
|
|
||||||
cargo-watch
|
|
||||||
samply
|
|
||||||
];
|
];
|
||||||
|
|
||||||
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${
|
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${
|
||||||
|
|
@ -111,6 +135,28 @@
|
||||||
pkgs.libclang
|
pkgs.libclang
|
||||||
]
|
]
|
||||||
}";
|
}";
|
||||||
|
|
||||||
|
commonArgs = {
|
||||||
|
strictDeps = false;
|
||||||
|
inherit src buildInputs nativeBuildInputs LD_LIBRARY_PATH;
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
|
lumina = craneLib.buildPackage (
|
||||||
|
commonArgs
|
||||||
|
// {
|
||||||
|
inherit cargoArtifacts buildInputs nativeBuildInputs LD_LIBRARY_PATH;
|
||||||
|
|
||||||
|
preBuild = ''
|
||||||
|
export DATABASE_URL=sqlite:./db.sqlite3
|
||||||
|
sqlx database create
|
||||||
|
sqlx migrate run
|
||||||
|
'';
|
||||||
|
cargoTestCommand = "";
|
||||||
|
cargoExtraArgs = "";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
devShell =
|
devShell =
|
||||||
|
|
@ -124,15 +170,12 @@
|
||||||
DATABASE_URL = "sqlite://./test.db";
|
DATABASE_URL = "sqlite://./test.db";
|
||||||
# RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
|
# RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
|
||||||
};
|
};
|
||||||
defaultPackage = naersk'.buildPackage {
|
defaultPackage = lumina;
|
||||||
inherit nativeBuildInputs buildInputs LD_LIBRARY_PATH;
|
|
||||||
src = ./.;
|
|
||||||
};
|
|
||||||
packages = {
|
packages = {
|
||||||
default = naersk'.buildPackage {
|
postInstall = ''
|
||||||
inherit nativeBuildInputs buildInputs LD_LIBRARY_PATH;
|
libcosmicAppWrapperArgs+=(--prefix GST_PLUGIN_SYSTEM_PATH_1_0 : "$GST_PLUGIN_SYSTEM_PATH_1_0")
|
||||||
src = ./.;
|
'';
|
||||||
};
|
default = lumina;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
71
justfile
|
|
@ -1,8 +1,8 @@
|
||||||
ui := "-i"
|
|
||||||
verbose := "-v"
|
verbose := "-v"
|
||||||
file := "~/dev/lumina-iced/test_presentation.lisp"
|
file := "~/dev/lumina-iced/test_presentation.lisp"
|
||||||
|
sdk-version := "25.08"
|
||||||
|
|
||||||
export RUSTC_WRAPPER := "sccache"
|
# export RUSTC_WRAPPER := "sccache"
|
||||||
# export RUST_LOG := "debug"
|
# export RUST_LOG := "debug"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -11,23 +11,29 @@ build:
|
||||||
cargo build
|
cargo build
|
||||||
build-release:
|
build-release:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
build-offline:
|
||||||
|
cargo build --release --offline
|
||||||
run:
|
run:
|
||||||
cargo run -- {{verbose}} {{ui}}
|
cargo run -- {{verbose}}
|
||||||
run-release:
|
run-release:
|
||||||
cargo run --release -- {{verbose}} {{ui}}
|
cargo run --release -- {{verbose}}
|
||||||
run-file:
|
run-file:
|
||||||
cargo run -- {{verbose}} {{ui}} {{file}}
|
cargo run -- {{verbose}} cli {{file}}
|
||||||
|
fix:
|
||||||
|
cargo clippy --fix --bin "lumina" -p lumina -- -W clippy::pedantic -W clippy::perf -W clippy::nursery -W clippy::unwrap_used
|
||||||
clean:
|
clean:
|
||||||
cargo clean
|
cargo clean
|
||||||
|
watch-clippy:
|
||||||
|
cargo watch --why -x "clippy --all-targets --all-features"
|
||||||
test:
|
test:
|
||||||
cargo nextest run
|
cargo nextest run
|
||||||
ci-test:
|
ci-test:
|
||||||
cargo nextest run -- --skip test_db_and_model --skip test_update --skip test_song_slide_speed --skip test_song_to_slide --skip test_song_from_db
|
cargo nextest run -- --skip test_db_and_model --skip test_update --skip test_song_slide_speed --skip test_song_to_slide --skip test_song_from_db --skip song_search
|
||||||
bench:
|
bench:
|
||||||
export NEXTEST_EXPERIMENTAL_BENCHMARKS=1
|
export NEXTEST_EXPERIMENTAL_BENCHMARKS=1
|
||||||
cargo nextest bench
|
cargo nextest bench
|
||||||
profile:
|
profile:
|
||||||
samply record cargo run --release -- {{verbose}} {{ui}}
|
samply record cargo run --release -- {{verbose}}
|
||||||
|
|
||||||
alias b := build
|
alias b := build
|
||||||
alias r := run
|
alias r := run
|
||||||
|
|
@ -35,3 +41,54 @@ alias br := build-release
|
||||||
alias rr := run-release
|
alias rr := run-release
|
||||||
alias rf := run-file
|
alias rf := run-file
|
||||||
alias c := clean
|
alias c := clean
|
||||||
|
|
||||||
|
##### Sets up and builds the exe installer with nsis
|
||||||
|
windows-packager:
|
||||||
|
cargo install cargo-packager --locked
|
||||||
|
cargo build --release
|
||||||
|
cargo packager --release -f nsis
|
||||||
|
|
||||||
|
##### Sets up and builds the macos bundle and dmg
|
||||||
|
mac-packager:
|
||||||
|
cargo install cargo-packager --locked
|
||||||
|
export PKG_CONFIG_PATH=/Library/Frameworks/GStreamer.framework/Versions/1.0/lib/pkgconfig
|
||||||
|
export PATH=/Library/Frameworks/GStreamer.framework/Versions/1.0/bin:$PATH
|
||||||
|
cargo build --release
|
||||||
|
install_name_tool -add_rpath @executable_path/../Frameworks/GStreamer.framework/Libraries target/release/lumina
|
||||||
|
cargo packager --release -f dmg
|
||||||
|
|
||||||
|
##### Sets up flatpak to be able to build the lumina flatpak using all the latest pieces
|
||||||
|
flatpak-setup: flatpak-install-sdk install-flatpak-builder-tools
|
||||||
|
git -C "cosmic-flatpak-runtime" pull || git clone https://github.com/pop-os/cosmic-flatpak-runtime.git "cosmic-flatpak-runtime"
|
||||||
|
cd cosmic-flatpak-runtime
|
||||||
|
flatpak-builder --install --user --force-clean build-dir cosmic-flatpak-runtime/com.system76.Cosmic.Sdk.json
|
||||||
|
flatpak-builder --install --user --force-clean build-dir cosmic-flatpak-runtime/com.system76.Cosmic.BaseApp.json
|
||||||
|
|
||||||
|
flatpak-install-sdk:
|
||||||
|
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
flatpak install --noninteractive --user flathub \
|
||||||
|
org.freedesktop.Platform//{{ sdk-version }} \
|
||||||
|
org.freedesktop.Platform.Locale//{{ sdk-version }} \
|
||||||
|
org.freedesktop.Sdk//{{ sdk-version }} \
|
||||||
|
org.freedesktop.Sdk.Locale//{{ sdk-version }} \
|
||||||
|
org.freedesktop.Sdk.Docs//{{ sdk-version }} \
|
||||||
|
org.freedesktop.Sdk.Debug//{{ sdk-version }} \
|
||||||
|
org.freedesktop.Sdk.Extension.rust-nightly//{{ sdk-version }} \
|
||||||
|
org.freedesktop.Sdk.Extension.llvm22//{{ sdk-version }}
|
||||||
|
|
||||||
|
install-flatpak-builder-tools:
|
||||||
|
rm -rf flatpak-builder-tools
|
||||||
|
git clone https://github.com/flatpak/flatpak-builder-tools --branch master --depth 1
|
||||||
|
# pip install aiohttp tomlkit # Would be needed without nix
|
||||||
|
|
||||||
|
flatpak-gen-manifest: install-flatpak-builder-tools
|
||||||
|
python3 flatpak-builder-tools/cargo/flatpak-cargo-generator.py Cargo.lock -o cargo-sources.json
|
||||||
|
|
||||||
|
flatpak-build:
|
||||||
|
flatpak-builder --install-deps-from=flathub --keep-build-dirs --install --user --force-clean build-dir xyz.cochrun.lumina.yml
|
||||||
|
|
||||||
|
flatpak-shell:
|
||||||
|
flatpak-builder --run build-dir xyz.cochrun.lumina.yml sh
|
||||||
|
|
||||||
|
alias fb := flatpak-build
|
||||||
|
alias fs := flatpak-setup
|
||||||
|
|
|
||||||
6
migrations/20260422171528_add_videos_to_songs.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE songs
|
||||||
|
ADD COLUMN lyric_video TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE songs
|
||||||
|
ADD COLUMN music_video TEXT;
|
||||||
20
migrations/20260527180153_access_time.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE songs
|
||||||
|
ADD COLUMN created_at INTEGER;
|
||||||
|
ALTER TABLE songs
|
||||||
|
ADD COLUMN accessed_at INTEGER;
|
||||||
|
|
||||||
|
ALTER TABLE images
|
||||||
|
ADD COLUMN created_at INTEGER;
|
||||||
|
ALTER TABLE images
|
||||||
|
ADD COLUMN accessed_at INTEGER;
|
||||||
|
|
||||||
|
ALTER TABLE videos
|
||||||
|
ADD COLUMN created_at INTEGER;
|
||||||
|
ALTER TABLE videos
|
||||||
|
ADD COLUMN accessed_at INTEGER;
|
||||||
|
|
||||||
|
ALTER TABLE presentations
|
||||||
|
ADD COLUMN created_at INTEGER;
|
||||||
|
ALTER TABLE presentations
|
||||||
|
ADD COLUMN accessed_at INTEGER;
|
||||||
|
|
@ -3,6 +3,12 @@
|
||||||
|
|
||||||
Lumina is a presentation app that works from a cli or a UI. The goal is that through a simple text file, you can describe an entire presentation and then load and control it either from the command line, or a UI. The UI also provides user friendly ways of creating the presentation to allow for flexibility for users to make something that works for regular folk as well as developers and nerds.
|
Lumina is a presentation app that works from a cli or a UI. The goal is that through a simple text file, you can describe an entire presentation and then load and control it either from the command line, or a UI. The UI also provides user friendly ways of creating the presentation to allow for flexibility for users to make something that works for regular folk as well as developers and nerds.
|
||||||
|
|
||||||
|
[[file:./res/images/screenshot_2026-05-03_08-23-08.png]]
|
||||||
|
|
||||||
|
[[file:./res/images/screenshot_2026-05-03_08-23-23.png]]
|
||||||
|
|
||||||
|
[[file:./res/images/screenshot_2026-05-03_08-16-59.png]]
|
||||||
|
|
||||||
* Why build this?
|
* Why build this?
|
||||||
Primarily, I don't think there is a good tool for this kind of thing on Linux. On Windows and Mac there is ProPresenter or Proclaim. Both amazing presentation software built for churches or worship centers and can be used by others for other things too, but incredible tools. I want to have a similar tool on Linux. The available tools out there now are often old, broken, or very difficult to use. I want something incredibly easy, with very sane or at least very customizable keyboard controls that allow me to quickly build a presentation and make it VERY easy to run it too.
|
Primarily, I don't think there is a good tool for this kind of thing on Linux. On Windows and Mac there is ProPresenter or Proclaim. Both amazing presentation software built for churches or worship centers and can be used by others for other things too, but incredible tools. I want to have a similar tool on Linux. The available tools out there now are often old, broken, or very difficult to use. I want something incredibly easy, with very sane or at least very customizable keyboard controls that allow me to quickly build a presentation and make it VERY easy to run it too.
|
||||||
|
|
||||||
|
|
|
||||||
8
res/icons/align-on-canvas.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 5.7226562 4 C 4.7682642 4 4 4.892 4 6 L 5 6 C 5 5.3542968 5.3913485 5 5.7226562 5 L 6 5 L 6 4 L 5.7226562 4 z M 8 4 L 8 5 L 11 5 L 11 4 L 8 4 z M 13 4 L 13 5 L 16 5 L 16 4 L 13 4 z M 18 4 L 18 5 L 18.277344 5 C 18.608652 5 19 5.3542968 19 6 L 20 6 C 20 4.892 19.231736 4 18.277344 4 L 18 4 z M 4 8 L 4 11 L 5 11 L 5 8 L 4 8 z M 7 8 L 7 16 L 17 16 L 17 8 L 7 8 z M 19 8 L 19 11 L 20 11 L 20 8 L 19 8 z M 4 13 L 4 16 L 5 16 L 5 13 L 4 13 z M 19 13 L 19 16 L 20 16 L 20 13 L 19 13 z M 4 18 C 4 19.108 4.7682642 20 5.7226562 20 L 6 20 L 6 19 L 5.7226562 19 C 5.3913486 19 5 18.645703 5 18 L 4 18 z M 19 18 C 19 18.645703 18.608652 19 18.277344 19 L 18 19 L 18 20 L 18.277344 20 C 19.231736 20 20 19.108 20 18 L 19 18 z M 8 19 L 8 20 L 11 20 L 11 19 L 8 19 z M 13 19 L 13 20 L 16 20 L 16 19 L 13 19 z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
10
res/icons/boundingbox_bottom.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 14,13 A 5,5 0 0 1 9,18 5,5 0 0 1 4,13 5,5 0 0 1 9,8 5,5 0 0 1 14,13 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 9,8 A 5,5 0 0 0 4,13 5,5 0 0 0 9,18 5,5 0 0 0 14,13 5,5 0 0 0 9,8 Z M 9,9 A 4,4 0 0 1 13,13 4,4 0 0 1 9,17 4,4 0 0 1 5,13 4,4 0 0 1 9,9 Z"/>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 0,12 H 18 V 14 H 0 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 832 B |
10
res/icons/boundingbox_bottom_left.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 10,13 A 5,5 0 0 1 5,18 5,5 0 0 1 0,13 5,5 0 0 1 5,8 5,5 0 0 1 10,13 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 5,8 A 5,5 0 0 0 0,13 5,5 0 0 0 5,18 5,5 0 0 0 10,13 5,5 0 0 0 5,8 Z M 5,9 A 4,4 0 0 1 9,13 4,4 0 0 1 5,17 4,4 0 0 1 1,13 4,4 0 0 1 5,9 Z"/>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 4,0 V 14 H 18 V 12 H 6 V 0 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 839 B |
10
res/icons/boundingbox_bottom_right.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 18,13 A 5,5 0 0 1 13,18 5,5 0 0 1 8,13 5,5 0 0 1 13,8 5,5 0 0 1 18,13 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 13,8 A 5,5 0 0 0 8,13 5,5 0 0 0 13,18 5,5 0 0 0 18,13 5,5 0 0 0 13,8 Z M 13,9 A 4,4 0 0 1 17,13 4,4 0 0 1 13,17 4,4 0 0 1 9,13 4,4 0 0 1 13,9 Z"/>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 14,0 V 14 H 0 V 12 H 12 V 0 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 849 B |
9
res/icons/boundingbox_center.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 9,4 A 5,5 0 0 0 4,9 5,5 0 0 0 9,14 5,5 0 0 0 14,9 5,5 0 0 0 9,4 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 9,4 A 5,5 0 0 0 4,9 5,5 0 0 0 9,14 5,5 0 0 0 14,9 5,5 0 0 0 9,4 Z M 9,5 A 4,4 0 0 1 13,9 4,4 0 0 1 9,13 4,4 0 0 1 5,9 4,4 0 0 1 9,5 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 737 B |
10
res/icons/boundingbox_left.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 10,9 A 5,5 0 0 1 5,14 5,5 0 0 1 0,9 5,5 0 0 1 5,4 5,5 0 0 1 10,9 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 5,4 A 5,5 0 0 0 0,9 5,5 0 0 0 5,14 5,5 0 0 0 10,9 5,5 0 0 0 5,4 Z M 5,5 A 4,4 0 0 1 9,9 4,4 0 0 1 5,13 4,4 0 0 1 1,9 4,4 0 0 1 5,5 Z"/>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 4,0 H 6 V 18 H 4 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 822 B |
10
res/icons/boundingbox_right.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 18,9 A 5,5 0 0 1 13,14 5,5 0 0 1 8,9 5,5 0 0 1 13,4 5,5 0 0 1 18,9 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 13,4 A 5,5 0 0 0 8,9 5,5 0 0 0 13,14 5,5 0 0 0 18,9 5,5 0 0 0 13,4 Z M 13,5 A 4,4 0 0 1 17,9 4,4 0 0 1 13,13 4,4 0 0 1 9,9 4,4 0 0 1 13,5 Z"/>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 12,0 H 14 V 18 H 12 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 834 B |
10
res/icons/boundingbox_top.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 14,5 A 5,5 0 0 1 9,10 5,5 0 0 1 4,5 5,5 0 0 1 9,0 5,5 0 0 1 14,5 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 9,0 A 5,5 0 0 0 4,5 5,5 0 0 0 9,10 5,5 0 0 0 14,5 5,5 0 0 0 9,0 Z M 9,1 A 4,4 0 0 1 13,5 4,4 0 0 1 9,9 4,4 0 0 1 5,5 4,4 0 0 1 9,1 Z"/>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 0,4 H 18 V 6 H 0 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 822 B |
10
res/icons/boundingbox_top_left.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 10,5 A 5,5 0 0 0 5,0 5,5 0 0 0 0,5 5,5 0 0 0 5,10 5,5 0 0 0 10,5 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 5,0 A 5,5 0 0 0 0,5 5,5 0 0 0 5,10 5,5 0 0 0 10,5 5,5 0 0 0 5,0 Z M 5,1 A 4,4 0 0 1 9,5 4,4 0 0 1 5,9 4,4 0 0 1 1,5 4,4 0 0 1 5,1 Z"/>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 4,18 V 4 H 18 V 6 H 6 V 18 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 831 B |
10
res/icons/boundingbox_top_right.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" version="1.1">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path style="opacity:.35;fill:currentColor" class="ColorScheme-Text" d="M 18,5 A 5,5 0 0 1 13,10 5,5 0 0 1 8,5 5,5 0 0 1 13,0 5,5 0 0 1 18,5 Z"/>
|
||||||
|
<path style="opacity:0.7;fill:currentColor" class="ColorScheme-Text" d="M 13,0 A 5,5 0 0 0 8,5 5,5 0 0 0 13,10 5,5 0 0 0 18,5 5,5 0 0 0 13,0 Z M 13,1 A 4,4 0 0 1 17,5 4,4 0 0 1 13,9 4,4 0 0 1 9,5 4,4 0 0 1 13,1 Z"/>
|
||||||
|
<path style="fill:currentColor" class="ColorScheme-Text" d="M 14,18 V 4 H 0 V 6 H 12 V 18 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 841 B |
1
res/icons/caret-left.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-caret-left"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M13.883 5.007l.058 -.005h.118l.058 .005l.06 .009l.052 .01l.108 .032l.067 .027l.132 .07l.09 .065l.081 .073l.083 .094l.054 .077l.054 .096l.017 .036l.027 .067l.032 .108l.01 .053l.01 .06l.004 .057l.002 .059v12c0 .852 -.986 1.297 -1.623 .783l-.084 -.076l-6 -6a1 1 0 0 1 -.083 -1.32l.083 -.094l6 -6l.094 -.083l.077 -.054l.096 -.054l.036 -.017l.067 -.027l.108 -.032l.053 -.01l.06 -.01z" /></svg>
|
||||||
|
After Width: | Height: | Size: 621 B |
1
res/icons/caret-right.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-caret-right"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M9 6c0 -.852 .986 -1.297 1.623 -.783l.084 .076l6 6a1 1 0 0 1 .083 1.32l-.083 .094l-6 6l-.094 .083l-.077 .054l-.096 .054l-.036 .017l-.067 .027l-.108 .032l-.053 .01l-.06 .01l-.057 .004l-.059 .002l-.059 -.002l-.058 -.005l-.06 -.009l-.052 -.01l-.108 -.032l-.067 -.027l-.132 -.07l-.09 -.065l-.081 -.073l-.083 -.094l-.054 -.077l-.054 -.096l-.017 -.036l-.027 -.067l-.032 -.108l-.01 -.053l-.01 -.06l-.004 -.057l-.002 -12.059z" /></svg>
|
||||||
|
After Width: | Height: | Size: 660 B |
5
res/icons/carousel.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<!-- <svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4 19h2c0 1.103.897 2 2 2h8c1.103 0 2-.897 2-2h2c1.103 0 2-.897 2-2V7c0-1.103-.897-2-2-2h-2c0-1.103-.897-2-2-2H8c-1.103 0-2 .897-2 2H4c-1.103 0-2 .897-2 2v10c0 1.103.897 2 2 2zM20 7v10h-2V7h2zM8 5h8l.001 14H8V5zM4 7h2v10H4V7z"/></svg> -->
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-carousel-horizontal"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 4h-8a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2z" /><path d="M22 6a1 1 0 0 1 .117 1.993l-.117 .007h-1v8h1a1 1 0 0 1 .117 1.993l-.117 .007h-1a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-8a2 2 0 0 1 1.85 -1.995l.15 -.005h1z" /><path d="M3 6a2 2 0 0 1 1.995 1.85l.005 .15v8a2 2 0 0 1 -1.85 1.995l-.15 .005h-1a1 1 0 0 1 -.117 -1.993l.117 -.007h1v-8h-1a1 1 0 0 1 -.117 -1.993l.117 -.007h1z" /></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
1
res/icons/circle-plus.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-circle-plus"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4.929 4.929a10 10 0 1 1 14.141 14.141a10 10 0 0 1 -14.14 -14.14m8.071 4.071a1 1 0 1 0 -2 0v2h-2a1 1 0 1 0 0 2h2v2a1 1 0 1 0 2 0v-2h2a1 1 0 1 0 0 -2h-2v-2z" /></svg>
|
||||||
|
After Width: | Height: | Size: 398 B |
1
res/icons/edit.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-edit"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M8 7a1 1 0 0 1 -1 1h-1a1 1 0 0 0 -1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1 -1v-1a1 1 0 0 1 2 0v1a3 3 0 0 1 -3 3h-9a3 3 0 0 1 -3 -3v-9a3 3 0 0 1 3 -3h1a1 1 0 0 1 1 1" /><path d="M14.596 5.011l4.392 4.392l-6.28 6.303a1 1 0 0 1 -.708 .294h-3a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 .294 -.708zm6.496 -2.103a3.097 3.097 0 0 1 .165 4.203l-.164 .18l-.693 .694l-4.387 -4.387l.695 -.69a3.1 3.1 0 0 1 4.384 0" /></svg>
|
||||||
|
After Width: | Height: | Size: 617 B |
3
res/icons/horizontal-squares.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?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">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 100"><path fill="#000000" d="M685 35h30v30h-30zM465 15h70v70h-70zM585 25h50v50h-50zM365 25h50v50h-50zM285 35h30v30h-30z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 331 B |
1
res/icons/layout-grid.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-layout-grid"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M9 3a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-4a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z" /><path d="M19 3a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-4a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z" /><path d="M9 13a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-4a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z" /><path d="M19 13a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-4a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z" /></svg>
|
||||||
|
After Width: | Height: | Size: 578 B |
|
Before Width: | Height: | Size: 1,022 B After Width: | Height: | Size: 1,022 B |
|
Before Width: | Height: | Size: 1,022 B After Width: | Height: | Size: 1,022 B |
BIN
res/icons/lumina.icns
Normal file
BIN
res/icons/lumina.ico
Normal file
|
After Width: | Height: | Size: 264 KiB |
105
res/icons/lumina.svg
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="300"
|
||||||
|
height="300"
|
||||||
|
viewBox="0 0 79.375 79.375"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||||
|
sodipodi:docname="app.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><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:document-units="mm"
|
||||||
|
inkscape:zoom="1.1436372"
|
||||||
|
inkscape:cx="50.715386"
|
||||||
|
inkscape:cy="218.1636"
|
||||||
|
inkscape:window-width="1504"
|
||||||
|
inkscape:window-height="950"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" /><defs
|
||||||
|
id="defs1"><linearGradient
|
||||||
|
id="linearGradient14"
|
||||||
|
inkscape:collect="always"><stop
|
||||||
|
style="stop-color:#ff9f43;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop14" /><stop
|
||||||
|
style="stop-color:#ff5c57;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop15" /></linearGradient><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient14"
|
||||||
|
id="linearGradient15"
|
||||||
|
x1="5259.6104"
|
||||||
|
y1="956.60291"
|
||||||
|
x2="5639.8418"
|
||||||
|
y2="11845.003"
|
||||||
|
gradientUnits="userSpaceOnUse" /><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient14"
|
||||||
|
id="linearGradient20"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="5259.6104"
|
||||||
|
y1="956.60291"
|
||||||
|
x2="5639.8418"
|
||||||
|
y2="11845.003" /><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient14"
|
||||||
|
id="linearGradient21"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="5259.6104"
|
||||||
|
y1="956.60291"
|
||||||
|
x2="5639.8418"
|
||||||
|
y2="11845.003" /></defs><g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"><g
|
||||||
|
id="g21"
|
||||||
|
transform="matrix(0.45359205,0,0,0.45359205,-8.9236096,-19.93096)"><rect
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:6;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||||
|
id="rect1"
|
||||||
|
width="154.52307"
|
||||||
|
height="91.384621"
|
||||||
|
x="29.907692"
|
||||||
|
y="63.553844"
|
||||||
|
rx="14.7" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:6.00001;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers"
|
||||||
|
id="rect2"
|
||||||
|
height="40.292309"
|
||||||
|
x="99.276917"
|
||||||
|
y="155.35384"
|
||||||
|
rx="3"
|
||||||
|
width="15.784616" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.04319;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers"
|
||||||
|
id="rect3"
|
||||||
|
width="66.877388"
|
||||||
|
height="8.9343357"
|
||||||
|
x="73.730537"
|
||||||
|
y="193.38441"
|
||||||
|
rx="1.2697047" /><g
|
||||||
|
transform="matrix(0.00516606,0,0,-0.00516606,79.015614,142.63061)"
|
||||||
|
fill="#000000"
|
||||||
|
stroke="none"
|
||||||
|
id="g2"
|
||||||
|
style="fill:url(#linearGradient15)"><path
|
||||||
|
d="m 3983,12758 c 550,-942 733,-1327 886,-1863 187,-652 256,-1456 222,-2600 -28,-968 -74,-1213 -436,-2320 -279,-853 -342,-1094 -387,-1470 -21,-180 -15,-672 16,-1230 15,-269 31,-557 35,-640 5,-82 7,-152 5,-154 -2,-2 -8,7 -13,20 -5,13 -72,166 -148,339 -194,437 -229,537 -283,799 -59,295 -80,574 -80,1101 0,649 28,1184 164,3150 l 53,765 -233,470 c -265,534 -323,641 -509,920 -396,597 -878,1127 -977,1074 -23,-13 -22,-57 7,-179 13,-59 16,-102 12,-200 -15,-341 -183,-817 -537,-1528 C 1601,8854 1498,8663 1200,8140 904,7620 799,7426 626,7080 464,6754 411,6640 320,6410 78,5800 -12,5297 4,4655 14,4233 50,3930 131,3576 396,2423 1085,1372 2116,546 2458,272 2733,110 2985,34 3069,9 3090,7 3305,3 3432,0 3551,0 3570,2 l 34,3 -284,310 c -157,171 -317,346 -356,390 -659,746 -1077,1679 -1198,2675 -30,249 -40,420 -41,710 0,303 5,348 42,385 22,23 39,18 84,-22 76,-69 111,-156 128,-323 18,-181 102,-410 290,-795 287,-587 653,-1174 974,-1565 89,-108 471,-489 647,-646 C 4238,815 4577,549 4925,315 5256,93 5427,34 5811,9 c 209,-13 815,-7 999,11 193,18 330,48 330,72 0,4 -67,142 -149,306 -647,1294 -691,1461 -678,2542 6,473 13,579 52,811 57,340 160,612 558,1480 197,429 314,695 406,920 375,924 501,1542 501,2449 0,360 -21,590 -81,903 -109,571 -366,1133 -725,1582 -119,150 -420,448 -582,577 -382,304 -752,511 -1447,807 -396,170 -561,226 -830,285 -88,20 -171,38 -184,41 l -23,6 z"
|
||||||
|
id="path1"
|
||||||
|
style="fill:url(#linearGradient20);fill-opacity:1" /><path
|
||||||
|
d="M 9451,8098 C 9338,7444 9273,7190 9129,6846 8989,6513 8820,6213 8360,5480 7868,4697 7545,4130 7439,3863 c -61,-155 -103,-386 -121,-665 -38,-621 109,-1454 367,-2079 54,-131 466,-979 475,-979 3,0 38,116 79,258 137,477 183,589 330,808 174,260 353,461 790,888 768,751 961,962 1156,1261 198,302 298,591 349,1010 39,321 45,970 11,1280 -54,498 -168,900 -379,1340 -146,304 -295,536 -696,1086 -150,206 -276,377 -279,382 -4,4 -35,-156 -70,-355 z"
|
||||||
|
id="path2"
|
||||||
|
style="fill:url(#linearGradient21);fill-opacity:1" /></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
1
res/icons/plus.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-plus"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||||
|
After Width: | Height: | Size: 348 B |
8
res/icons/presenting.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48" id="Presentation--Streamline-Plump-Remix" height="48" width="48">
|
||||||
|
<desc>
|
||||||
|
Presentation Streamline Icon: https://streamlinehq.com
|
||||||
|
</desc>
|
||||||
|
<g id="presentation">
|
||||||
|
<path id="Union" fill="#000000" fill-rule="evenodd" d="M7.68448 4.93327C10.5616 4.72445 15.6087 4.5 24 4.5s13.4384 0.22445 16.3155 0.43327c1.2874 0.09344 2.159 0.94259 2.2891 2.11111C42.8112 8.89951 43 11.649 43 15.4999c0 3.851 -0.1888 6.6005 -0.3954 8.4557 -0.13 1.1676 -1.0025 2.0178 -2.2908 2.1113 -0.1441 0.0104 -0.2937 0.0209 -0.4489 0.0314 -1.1021 0.0746 -1.9349 1.0285 -1.8603 2.1306 0.0746 1.102 1.0285 1.9349 2.1305 1.8603 0.1615 -0.011 0.3175 -0.0219 0.4682 -0.0328 3.0935 -0.2245 5.6206 -2.4595 5.9768 -5.6582 0.2248 -2.0194 0.4199 -4.9179 0.4199 -8.8983 0 -3.9803 -0.1951 -6.87878 -0.4199 -8.89816 -0.3561 -3.19776 -2.8807 -5.43339 -5.975 -5.657976C37.6048 0.726004 32.4567 0.5 24 0.5S10.3952 0.726004 7.39492 0.943764C4.30058 1.16835 1.77599 3.40398 1.41994 6.60174 1.19509 8.62112 1 11.5196 1 15.4999c0 3.9804 0.19509 6.8789 0.41994 8.8983 0.35615 3.1987 2.88331 5.4337 5.97679 5.6582 0.15063 0.0109 0.30664 0.0218 0.46816 0.0328 1.10205 0.0746 2.05592 -0.7583 2.13054 -1.8603 0.07467 -1.1021 -0.75827 -2.056 -1.86032 -2.1306 -0.15521 -0.0105 -0.3048 -0.021 -0.44891 -0.0314 -1.28831 -0.0935 -2.16082 -0.9437 -2.29083 -2.1113C5.18881 22.1004 5 19.3509 5 15.4999c0 -3.8509 0.18881 -6.60039 0.39537 -8.45552 0.13011 -1.16852 1.00171 -2.01767 2.28911 -2.11111Zm0.93583 27.68253C10.7965 32.5742 15.8266 32.5 24 32.5s13.2035 0.0742 15.3797 0.1158c1.1393 0.0218 2.3245 0.7513 2.5097 2.11C41.9536 35.197 42 35.785 42 36.5c0 0.715 -0.0464 1.303 -0.1106 1.7742 -0.1852 1.3587 -1.3704 2.0882 -2.5097 2.11 -0.2919 0.0056 -0.6352 0.0117 -1.0306 0.0182l-1.3047 5.8165c-0.1025 0.4566 -0.5078 0.7811 -0.9758 0.7811H11.931c-0.468 0 -0.8734 -0.3245 -0.9758 -0.7811l-1.30477 -5.8165c-0.39516 -0.0065 -0.7383 -0.0126 -1.03012 -0.0182 -1.13927 -0.0218 -2.3245 -0.7513 -2.50968 -2.11C6.0464 37.803 6 37.215 6 36.5c0 -0.715 0.0464 -1.303 0.11063 -1.7742 0.18518 -1.3587 1.37041 -2.0882 2.50968 -2.11ZM17.5 16.5c0 -3.5899 2.9102 -6.5 6.5 -6.5 3.5899 0 6.5 2.9101 6.5 6.5 0 2.4056 -1.3068 4.5059 -3.2492 5.6299 1.2406 0.1048 2.2521 0.2613 3.0527 0.425 1.72 0.3516 2.9398 1.6803 3.3813 3.322 0.2504 0.9311 0.5364 2.12 0.8063 3.5291 0.028 0.1464 -0.0107 0.2977 -0.1057 0.4126S34.1491 30 34 30h-8.25v-3.5c0 -0.9665 -0.7835 -1.75 -1.75 -1.75s-1.75 0.7835 -1.75 1.75V30H14c-0.1491 0 -0.2904 -0.0665 -0.3854 -0.1814 -0.095 -0.1149 -0.1337 -0.2662 -0.1057 -0.4126 0.2698 -1.4089 0.5559 -2.5977 0.8062 -3.5287 0.4416 -1.642 1.6616 -2.9708 3.3818 -3.3224 0.8006 -0.1637 1.812 -0.3201 3.0524 -0.4249C18.8068 21.006 17.5 18.9056 17.5 16.5Z" clip-rule="evenodd" stroke-width="1"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
14
res/icons/preview.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" id="icon" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<circle cx="16" cy="19" r="2"/>
|
||||||
|
<path d="M23.7769,18.4785A8.64,8.64,0,0,0,16,13a8.64,8.64,0,0,0-7.7769,5.4785L8,19l.2231.5215A8.64,8.64,0,0,0,16,25a8.64,8.64,0,0,0,7.7769-5.4785L24,19ZM16,23a4,4,0,1,1,4-4A4.0045,4.0045,0,0,1,16,23Z"/>
|
||||||
|
<path d="M27,3H5A2,2,0,0,0,3,5V27a2,2,0,0,0,2,2H27a2,2,0,0,0,2-2V5A2,2,0,0,0,27,3ZM5,5H27V9H5ZM5,27V11H27V27Z"/>
|
||||||
|
<rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" class="cls-1" width="32" height="32"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 799 B |
1
res/icons/search.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-search"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||||
|
After Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
8
res/icons/slides.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0" fill="none" width="20" height="20"/>
|
||||||
|
<g>
|
||||||
|
<path d="M5 14V6h10v8H5zm-3-1V7h2v6H2zm4-6v6h8V7H6zm10 0h2v6h-2V7zm-3 2V8H7v1h6zm0 3v-2H7v2h6z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 373 B |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 976 B After Width: | Height: | Size: 976 B |
1
res/icons/x.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>
|
||||||
|
After Width: | Height: | Size: 347 B |
BIN
res/images/screenshot_2026-05-03_08-16-59.png
Normal file
|
After Width: | Height: | Size: 541 KiB |
BIN
res/images/screenshot_2026-05-03_08-23-08.png
Normal file
|
After Width: | Height: | Size: 398 KiB |
BIN
res/images/screenshot_2026-05-03_08-23-23.png
Normal file
|
After Width: | Height: | Size: 558 KiB |
11
res/xyz.cochrun.lumina.desktop
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
|
||||||
|
Name=Lumina
|
||||||
|
Comment=A church presentation app that is built to be simple to use.
|
||||||
|
Categories=Graphics;
|
||||||
|
|
||||||
|
Icon=lumina
|
||||||
|
Exec=lumina
|
||||||
|
Terminal=false
|
||||||
49
res/xyz.cochrun.lumina.metainfo.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>xyz.cochrun.lumina</id>
|
||||||
|
|
||||||
|
<name>Lumina</name>
|
||||||
|
<summary>A church presentation app that is built to be simple to use.</summary>
|
||||||
|
<url type="homepage">https://git.tfcconnection.org/chris/lumina/</url>
|
||||||
|
|
||||||
|
<metadata_license>FSFAP</metadata_license>
|
||||||
|
<project_license>GPL-3.0-or-later</project_license>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
A church presentation app that is built to be simple to use.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<developer id="xyz.cochrun">
|
||||||
|
<name>Chris Cochrun</name>
|
||||||
|
</developer>
|
||||||
|
|
||||||
|
<launchable type="desktop-id">xyz.cochrun.lumina.desktop</launchable>
|
||||||
|
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<image>https://git.tfcconnection.org/chris/lumina/raw/branch/master/res/images/screenshot_2026-05-03_08-16-59.png</image>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://git.tfcconnection.org/chris/lumina/raw/branch/master/res/images/screenshot_2026-05-03_08-23-08.png</image>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://git.tfcconnection.org/chris/lumina/raw/branch/master/res/images/screenshot_2026-05-03_08-23-23.png</image>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
<categories>
|
||||||
|
<category>Graphics</category>
|
||||||
|
</categories>
|
||||||
|
<keywords>
|
||||||
|
<keyword>presentation</keyword>
|
||||||
|
<keyword>photo</keyword>
|
||||||
|
<keyword>video</keyword>
|
||||||
|
<keyword>cosmic</keyword>
|
||||||
|
</keywords>
|
||||||
|
<supports>
|
||||||
|
<control>pointing</control>
|
||||||
|
<control>keyboard</control>
|
||||||
|
<control>touch</control>
|
||||||
|
</supports>
|
||||||
|
</component>
|
||||||
12
rust-analyzer.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# [lru]
|
||||||
|
# capacity = 64
|
||||||
|
|
||||||
|
[semanticHighlighting]
|
||||||
|
operator.enable = false
|
||||||
|
punctuation.enable = false
|
||||||
|
strings.enable = false
|
||||||
|
nonStandardTokens = false
|
||||||
|
|
||||||
|
[files]
|
||||||
|
exclude = ["flatpak-builder-tools", "cosmic-flatpak-runtime",
|
||||||
|
"flatpak-out", "mupdf", "build-dir", ".zed", "mupdf-cargo-sources.json", "cargo-sources.json"]
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
max_width = 70
|
max_width = 90
|
||||||
style_edition = "2024"
|
style_edition = "2024"
|
||||||
# version = "Two"
|
# version = "Two"
|
||||||
|
imports_granularity = "Module"
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::Background;
|
use crate::Background;
|
||||||
|
|
||||||
use super::{kinds::ServiceItemKind, service_items::ServiceItem};
|
use super::kinds::ServiceItemKind;
|
||||||
|
use super::service_items::ServiceItem;
|
||||||
|
|
||||||
pub trait Content {
|
pub trait Content {
|
||||||
fn title(&self) -> String;
|
fn title(&self) -> String;
|
||||||
|
|
|
||||||
281
src/core/file.rs
|
|
@ -1,22 +1,20 @@
|
||||||
use crate::core::{
|
use crate::core::kinds::ServiceItemKind;
|
||||||
kinds::ServiceItemKind, service_items::ServiceItem,
|
use crate::core::service_items::ServiceItem;
|
||||||
slide::Background,
|
use crate::core::slide::Background;
|
||||||
};
|
|
||||||
use cosmic::widget::image::Handle;
|
use cosmic::widget::image::Handle;
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
use miette::{IntoDiagnostic, Result, miette};
|
||||||
use std::{
|
use std::fs::{self, File};
|
||||||
fs::{self, File},
|
use std::io::Write;
|
||||||
io::Write,
|
use std::iter;
|
||||||
iter,
|
use std::path::{Path, PathBuf};
|
||||||
path::{Path, PathBuf},
|
use std::sync::Arc;
|
||||||
};
|
|
||||||
use tar::{Archive, Builder};
|
use tar::{Archive, Builder};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
use zstd::{Decoder, Encoder};
|
use zstd::{Decoder, Encoder};
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn save(
|
pub fn save(
|
||||||
list: Vec<ServiceItem>,
|
list: &Arc<Vec<ServiceItem>>,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -26,8 +24,7 @@ pub fn save(
|
||||||
}
|
}
|
||||||
let save_file = File::create(path).into_diagnostic()?;
|
let save_file = File::create(path).into_diagnostic()?;
|
||||||
let ron_pretty = ron::ser::PrettyConfig::default();
|
let ron_pretty = ron::ser::PrettyConfig::default();
|
||||||
let ron = ron::ser::to_string_pretty(&list, ron_pretty)
|
let ron = ron::ser::to_string_pretty(&list, ron_pretty).into_diagnostic()?;
|
||||||
.into_diagnostic()?;
|
|
||||||
|
|
||||||
let encoder = Encoder::new(save_file, 3)
|
let encoder = Encoder::new(save_file, 3)
|
||||||
.expect("file encoder shouldn't fail")
|
.expect("file encoder shouldn't fail")
|
||||||
|
|
@ -37,8 +34,7 @@ pub fn save(
|
||||||
"there should be a data directory, ~/.local/share/ for linux, but couldn't find it",
|
"there should be a data directory, ~/.local/share/ for linux, but couldn't find it",
|
||||||
);
|
);
|
||||||
temp_dir.push("lumina");
|
temp_dir.push("lumina");
|
||||||
let mut s: String =
|
let mut s: String = iter::repeat_with(fastrand::alphanumeric).take(5).collect();
|
||||||
iter::repeat_with(fastrand::alphanumeric).take(5).collect();
|
|
||||||
s.insert_str(0, "temp_");
|
s.insert_str(0, "temp_");
|
||||||
temp_dir.push(s);
|
temp_dir.push(s);
|
||||||
fs::create_dir_all(&temp_dir).into_diagnostic()?;
|
fs::create_dir_all(&temp_dir).into_diagnostic()?;
|
||||||
|
|
@ -62,9 +58,7 @@ pub fn save(
|
||||||
}
|
}
|
||||||
match tar.append_file("serviceitems.ron", &mut f) {
|
match tar.append_file("serviceitems.ron", &mut f) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
debug!(
|
debug!("should have added serviceitems.ron to the file");
|
||||||
"should have added serviceitems.ron to the file"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(?e);
|
error!(?e);
|
||||||
|
|
@ -85,7 +79,7 @@ pub fn save(
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
for item in list {
|
for item in list.iter() {
|
||||||
let background;
|
let background;
|
||||||
let audio: Option<PathBuf>;
|
let audio: Option<PathBuf>;
|
||||||
match &item.kind {
|
match &item.kind {
|
||||||
|
|
@ -94,23 +88,18 @@ pub fn save(
|
||||||
audio = song.audio.clone();
|
audio = song.audio.clone();
|
||||||
}
|
}
|
||||||
ServiceItemKind::Image(image) => {
|
ServiceItemKind::Image(image) => {
|
||||||
background = Some(
|
background =
|
||||||
Background::try_from(image.path.clone())
|
Some(Background::try_from(image.path.clone()).into_diagnostic()?);
|
||||||
.into_diagnostic()?,
|
|
||||||
);
|
|
||||||
audio = None;
|
audio = None;
|
||||||
}
|
}
|
||||||
ServiceItemKind::Video(video) => {
|
ServiceItemKind::Video(video) => {
|
||||||
background = Some(
|
background =
|
||||||
Background::try_from(video.path.clone())
|
Some(Background::try_from(video.path.clone()).into_diagnostic()?);
|
||||||
.into_diagnostic()?,
|
|
||||||
);
|
|
||||||
audio = None;
|
audio = None;
|
||||||
}
|
}
|
||||||
ServiceItemKind::Presentation(presentation) => {
|
ServiceItemKind::Presentation(presentation) => {
|
||||||
background = Some(
|
background = Some(
|
||||||
Background::try_from(presentation.path.clone())
|
Background::try_from(presentation.path.clone()).into_diagnostic()?,
|
||||||
.into_diagnostic()?,
|
|
||||||
);
|
);
|
||||||
audio = None;
|
audio = None;
|
||||||
}
|
}
|
||||||
|
|
@ -131,11 +120,11 @@ pub fn save(
|
||||||
debug!(?path);
|
debug!(?path);
|
||||||
append_file(path)?;
|
append_file(path)?;
|
||||||
}
|
}
|
||||||
for slide in item.slides {
|
for slide in &item.slides {
|
||||||
if let Some(svg) = slide.text_svg
|
if let Some(svg) = &slide.text_svg
|
||||||
&& let Some(path) = svg.path
|
&& let Some(path) = &svg.path
|
||||||
{
|
{
|
||||||
append_file(path)?;
|
append_file(path.clone())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,12 +142,10 @@ pub fn save(
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
let decoder =
|
let decoder =
|
||||||
Decoder::new(fs::File::open(&path).into_diagnostic()?)
|
Decoder::new(fs::File::open(&path).into_diagnostic()?).into_diagnostic()?;
|
||||||
.into_diagnostic()?;
|
|
||||||
let mut tar = Archive::new(decoder);
|
let mut tar = Archive::new(decoder);
|
||||||
|
|
||||||
let mut cache_dir =
|
let mut cache_dir = dirs::cache_dir().expect("Should be a cache dir");
|
||||||
dirs::cache_dir().expect("Should be a cache dir");
|
|
||||||
cache_dir.push("lumina");
|
cache_dir.push("lumina");
|
||||||
cache_dir.push("cached_save_files");
|
cache_dir.push("cached_save_files");
|
||||||
|
|
||||||
|
|
@ -175,8 +162,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
.to_os_string()
|
.to_os_string()
|
||||||
.into_string()
|
.into_string()
|
||||||
.expect("Should be fine");
|
.expect("Should be fine");
|
||||||
let save_name = save_name_string
|
let save_name = save_name_string.trim_end_matches(&format!(".{save_name_ext}"));
|
||||||
.trim_end_matches(&format!(".{save_name_ext}"));
|
|
||||||
cache_dir.push(save_name);
|
cache_dir.push(save_name);
|
||||||
|
|
||||||
if let Err(e) = fs::remove_dir_all(&cache_dir) {
|
if let Err(e) = fs::remove_dir_all(&cache_dir) {
|
||||||
|
|
@ -192,9 +178,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
let mut dir = fs::read_dir(&cache_dir).into_diagnostic()?;
|
let mut dir = fs::read_dir(&cache_dir).into_diagnostic()?;
|
||||||
let ron_file = dir
|
let ron_file = dir
|
||||||
.find_map(|file| {
|
.find_map(|file| {
|
||||||
if file.as_ref().ok()?.path().extension()?.to_str()?
|
if file.as_ref().ok()?.path().extension()?.to_str()? == "ron" {
|
||||||
== "ron"
|
|
||||||
{
|
|
||||||
Some(file.ok()?.path())
|
Some(file.ok()?.path())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
@ -202,12 +186,10 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
})
|
})
|
||||||
.expect("Should have a ron file");
|
.expect("Should have a ron file");
|
||||||
|
|
||||||
let ron_string =
|
let ron_string = fs::read_to_string(ron_file).into_diagnostic()?;
|
||||||
fs::read_to_string(ron_file).into_diagnostic()?;
|
|
||||||
|
|
||||||
let mut items =
|
let mut items =
|
||||||
ron::de::from_str::<Vec<ServiceItem>>(&ron_string)
|
ron::de::from_str::<Vec<ServiceItem>>(&ron_string).into_diagnostic()?;
|
||||||
.into_diagnostic()?;
|
|
||||||
|
|
||||||
for item in &mut items {
|
for item in &mut items {
|
||||||
let dir = fs::read_dir(&cache_dir).into_diagnostic()?;
|
let dir = fs::read_dir(&cache_dir).into_diagnostic()?;
|
||||||
|
|
@ -215,33 +197,20 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
for slide in &mut item.slides {
|
for slide in &mut item.slides {
|
||||||
if let Ok(file) = file.as_ref() {
|
if let Ok(file) = file.as_ref() {
|
||||||
let file_name = file.file_name();
|
let file_name = file.file_name();
|
||||||
let audio_path =
|
let audio_path = slide.audio().clone().unwrap_or_default();
|
||||||
slide.audio().clone().unwrap_or_default();
|
let text_path =
|
||||||
let text_path = slide
|
slide.text_svg.as_ref().and_then(|svg| svg.path.clone());
|
||||||
.text_svg
|
if Some(file_name.as_os_str()) == slide.background.path.file_name() {
|
||||||
.as_ref()
|
|
||||||
.and_then(|svg| svg.path.clone());
|
|
||||||
if Some(file_name.as_os_str())
|
|
||||||
== slide.background.path.file_name()
|
|
||||||
{
|
|
||||||
slide.background.path = file.path();
|
slide.background.path = file.path();
|
||||||
} else if Some(file_name.as_os_str())
|
} else if Some(file_name.as_os_str()) == audio_path.file_name() {
|
||||||
== audio_path.file_name()
|
let new_slide = slide.clone().set_audio(Some(file.path()));
|
||||||
{
|
|
||||||
let new_slide = slide
|
|
||||||
.clone()
|
|
||||||
.set_audio(Some(file.path()));
|
|
||||||
*slide = new_slide;
|
*slide = new_slide;
|
||||||
} else if Some(file_name.as_os_str())
|
} else if Some(file_name.as_os_str())
|
||||||
== text_path
|
== text_path.clone().unwrap_or_default().file_name()
|
||||||
.clone()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.file_name()
|
|
||||||
&& let Some(svg) = slide.text_svg.as_mut()
|
&& let Some(svg) = slide.text_svg.as_mut()
|
||||||
{
|
{
|
||||||
svg.path = Some(file.path());
|
svg.path = Some(file.path());
|
||||||
svg.handle =
|
svg.handle = Some(Handle::from_path(file.path()));
|
||||||
Some(Handle::from_path(file.path()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,8 +219,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
ServiceItemKind::Song(song) => {
|
ServiceItemKind::Song(song) => {
|
||||||
if let Ok(file) = file.as_ref() {
|
if let Ok(file) = file.as_ref() {
|
||||||
let file_name = file.file_name();
|
let file_name = file.file_name();
|
||||||
let audio_path =
|
let audio_path = song.audio.clone().unwrap_or_default();
|
||||||
song.audio.clone().unwrap_or_default();
|
|
||||||
if Some(file_name.as_os_str())
|
if Some(file_name.as_os_str())
|
||||||
== song
|
== song
|
||||||
.background
|
.background
|
||||||
|
|
@ -261,14 +229,11 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
.file_name()
|
.file_name()
|
||||||
{
|
{
|
||||||
let background = song.background.clone();
|
let background = song.background.clone();
|
||||||
song.background =
|
song.background = background.map(|mut background| {
|
||||||
background.map(|mut background| {
|
background.path = file.path();
|
||||||
background.path = file.path();
|
background
|
||||||
background
|
});
|
||||||
});
|
} else if Some(file_name.as_os_str()) == audio_path.file_name() {
|
||||||
} else if Some(file_name.as_os_str())
|
|
||||||
== audio_path.file_name()
|
|
||||||
{
|
|
||||||
song.audio = Some(file.path());
|
song.audio = Some(file.path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -276,9 +241,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
ServiceItemKind::Video(video) => {
|
ServiceItemKind::Video(video) => {
|
||||||
if let Ok(file) = file.as_ref() {
|
if let Ok(file) = file.as_ref() {
|
||||||
let file_name = file.file_name();
|
let file_name = file.file_name();
|
||||||
if Some(file_name.as_os_str())
|
if Some(file_name.as_os_str()) == video.path.file_name() {
|
||||||
== video.path.file_name()
|
|
||||||
{
|
|
||||||
video.path = file.path();
|
video.path = file.path();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,9 +249,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
ServiceItemKind::Image(image) => {
|
ServiceItemKind::Image(image) => {
|
||||||
if let Ok(file) = file.as_ref() {
|
if let Ok(file) = file.as_ref() {
|
||||||
let file_name = file.file_name();
|
let file_name = file.file_name();
|
||||||
if Some(file_name.as_os_str())
|
if Some(file_name.as_os_str()) == image.path.file_name() {
|
||||||
== image.path.file_name()
|
|
||||||
{
|
|
||||||
image.path = file.path();
|
image.path = file.path();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -296,9 +257,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||||
ServiceItemKind::Presentation(presentation) => {
|
ServiceItemKind::Presentation(presentation) => {
|
||||||
if let Ok(file) = file.as_ref() {
|
if let Ok(file) = file.as_ref() {
|
||||||
let file_name = file.file_name();
|
let file_name = file.file_name();
|
||||||
if Some(file_name.as_os_str())
|
if Some(file_name.as_os_str()) == presentation.path.file_name() {
|
||||||
== presentation.path.file_name()
|
|
||||||
{
|
|
||||||
presentation.path = file.path();
|
presentation.path = file.path();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -316,20 +275,18 @@ mod test {
|
||||||
use resvg::usvg::fontdb;
|
use resvg::usvg::fontdb;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::core::service_items::ServiceTrait;
|
||||||
core::{
|
use crate::core::slide::{Slide, TextAlignment};
|
||||||
service_items::ServiceTrait,
|
use crate::core::songs::{Song, VerseName};
|
||||||
slide::{Slide, TextAlignment},
|
use crate::ui::text_svg::text_svg_generator;
|
||||||
songs::{Song, VerseName},
|
use std::collections::HashMap;
|
||||||
},
|
use std::path::PathBuf;
|
||||||
ui::text_svg::text_svg_generator,
|
use std::sync::Arc;
|
||||||
};
|
|
||||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
fn test_song() -> Song {
|
fn test_song() -> Song {
|
||||||
let lyrics = "Some({Verse(number:4):\"Our Savior displayed\\nOn a criminal\\'s cross\\n\\nDarkness rejoiced as though\\nHeaven had lost\\n\\nBut then Jesus arose\\nWith our freedom in hand\\n\\nThat\\'s when death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Intro(number:1):\"Death Was Arrested\\nNorth Point Worship\",Verse(number:3):\"Released from my chains,\\nI\\'m a prisoner no more\\n\\nMy shame was a ransom\\nHe faithfully bore\\n\\nHe cancelled my debt and\\nHe called me His friend\\n\\nWhen death was arrested\\nAnd my life began\",Bridge(number:1):\"Oh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\\n\\nOh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\",Other(number:99):\"When death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Verse(number:2):\"Ash was redeemed\\nOnly beauty remains\\n\\nMy orphan heart\\nWas given a name\\n\\nMy mourning grew quiet,\\nMy feet rose to dance\\n\\nWhen death was arrested\\nAnd my life began\",Verse(number:1):\"Alone in my sorrow\\nAnd dead in my sin\\n\\nLost without hope\\nWith no place to begin\\n\\nYour love made a way\\nTo let mercy come in\\n\\nWhen death was arrested\\nAnd my life began\",Chorus(number:1):\"Oh, Your grace so free,\\nWashes over me\\n\\nYou have made me new,\\nNow life begins with You\\n\\nIt\\'s Your endless love,\\nPouring down on us\\n\\nYou have made us new,\\nNow life begins with You\"})".to_string();
|
let lyrics = "Some({Verse(number:4):\"Our Savior displayed\\nOn a criminal\\'s cross\\n\\nDarkness rejoiced as though\\nHeaven had lost\\n\\nBut then Jesus arose\\nWith our freedom in hand\\n\\nThat\\'s when death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Intro(number:1):\"Death Was Arrested\\nNorth Point Worship\",Verse(number:3):\"Released from my chains,\\nI\\'m a prisoner no more\\n\\nMy shame was a ransom\\nHe faithfully bore\\n\\nHe cancelled my debt and\\nHe called me His friend\\n\\nWhen death was arrested\\nAnd my life began\",Bridge(number:1):\"Oh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\\n\\nOh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\",Other(number:99):\"When death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Verse(number:2):\"Ash was redeemed\\nOnly beauty remains\\n\\nMy orphan heart\\nWas given a name\\n\\nMy mourning grew quiet,\\nMy feet rose to dance\\n\\nWhen death was arrested\\nAnd my life began\",Verse(number:1):\"Alone in my sorrow\\nAnd dead in my sin\\n\\nLost without hope\\nWith no place to begin\\n\\nYour love made a way\\nTo let mercy come in\\n\\nWhen death was arrested\\nAnd my life began\",Chorus(number:1):\"Oh, Your grace so free,\\nWashes over me\\n\\nYou have made me new,\\nNow life begins with You\\n\\nIt\\'s Your endless love,\\nPouring down on us\\n\\nYou have made us new,\\nNow life begins with You\"})".to_string();
|
||||||
let verse_map: Option<HashMap<VerseName, String>> =
|
let verse_map: Option<HashMap<VerseName, String>> =
|
||||||
ron::from_str(&lyrics).unwrap();
|
ron::from_str(&lyrics).expect("");
|
||||||
Song {
|
Song {
|
||||||
id: 7,
|
id: 7,
|
||||||
title: "Death Was Arrested".to_string(),
|
title: "Death Was Arrested".to_string(),
|
||||||
|
|
@ -340,7 +297,7 @@ mod test {
|
||||||
ccli: None,
|
ccli: None,
|
||||||
audio: Some("/home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3".into()),
|
audio: Some("/home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3".into()),
|
||||||
verse_order: Some(vec!["Some([Chorus(number:1),Intro(number:1),Other(number:99),Bridge(number:1),Verse(number:4),Verse(number:2),Verse(number:3),Verse(number:1)])".to_string()]),
|
verse_order: Some(vec!["Some([Chorus(number:1),Intro(number:1),Other(number:99),Bridge(number:1),Verse(number:4),Verse(number:2),Verse(number:3),Verse(number:1)])".to_string()]),
|
||||||
background: Some(Background::try_from("/home/chris/nc/tfc/openlp/Flood/motions/Ocean_Floor_HD.mp4").unwrap()),
|
background: Some(Background::try_from("/home/chris/nc/tfc/presentations/mb/Geo Square.mp4").expect("")),
|
||||||
text_alignment: Some(TextAlignment::MiddleCenter),
|
text_alignment: Some(TextAlignment::MiddleCenter),
|
||||||
font: None,
|
font: None,
|
||||||
font_size: Some(120),
|
font_size: Some(120),
|
||||||
|
|
@ -362,20 +319,12 @@ mod test {
|
||||||
let fontdb = Arc::new(fontdb);
|
let fontdb = Arc::new(fontdb);
|
||||||
let slides = song
|
let slides = song
|
||||||
.to_slides()
|
.to_slides()
|
||||||
.unwrap()
|
.expect("")
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|slide| {
|
.map(|slide| {
|
||||||
text_svg_generator(
|
text_svg_generator(slide, &Arc::clone(&fontdb)).unwrap_or_else(|e| {
|
||||||
slide.clone(),
|
panic!("Couldn't create svg: {e}");
|
||||||
&Arc::clone(&fontdb),
|
})
|
||||||
)
|
|
||||||
.map_or_else(
|
|
||||||
|e| {
|
|
||||||
assert!(false, "Couldn't create svg: {e}");
|
|
||||||
slide
|
|
||||||
},
|
|
||||||
|slide| slide,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<Slide>>();
|
.collect::<Vec<Slide>>();
|
||||||
let items = vec![
|
let items = vec![
|
||||||
|
|
@ -391,7 +340,7 @@ mod test {
|
||||||
kind: ServiceItemKind::Song(song),
|
kind: ServiceItemKind::Song(song),
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Death was Arrested".into(),
|
title: "Death was Arrested".into(),
|
||||||
slides: slides,
|
slides,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
items
|
items
|
||||||
|
|
@ -404,7 +353,7 @@ mod test {
|
||||||
let result = load(&path);
|
let result = load(&path);
|
||||||
match result {
|
match result {
|
||||||
Ok(items) => {
|
Ok(items) => {
|
||||||
assert!(items.len() > 0);
|
assert!(!items.is_empty());
|
||||||
// assert_eq!(items, get_items());
|
// assert_eq!(items, get_items());
|
||||||
let cache_dir = cache_dir();
|
let cache_dir = cache_dir();
|
||||||
assert!(fs::read_dir(&cache_dir).is_ok());
|
assert!(fs::read_dir(&cache_dir).is_ok());
|
||||||
|
|
@ -415,37 +364,58 @@ mod test {
|
||||||
find_svgs(&items)?;
|
find_svgs(&items)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(format!("Error in the loading process: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_svgs(items: &Vec<ServiceItem>) -> Result<(), String> {
|
fn test_size_and_cache(mut path: PathBuf) -> Result<(), String> {
|
||||||
let cache_dir = cache_dir();
|
let cache_dir = cache_dir();
|
||||||
|
|
||||||
|
if path.metadata().expect("").len() < 15000 {
|
||||||
|
return Err(String::from(
|
||||||
|
"SVG text is too small, maybe the svg didn't generate properly",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if path.pop() && path == cache_dir {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(String::from(
|
||||||
|
"The path of the TextSvg isn't in the load directory",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_svgs(items: &[ServiceItem]) -> Result<(), String> {
|
||||||
items.iter().try_for_each(|item| {
|
items.iter().try_for_each(|item| {
|
||||||
if let ServiceItemKind::Song(..) = item.kind {
|
if let ServiceItemKind::Song(..) = item.kind {
|
||||||
item.slides.iter().try_for_each(|slide| {
|
item.slides.iter().try_for_each(|slide| {
|
||||||
slide.text_svg.as_ref().map_or(Err(String::from("There is no TextSvg for this song")), |text_svg| {
|
slide.text_svg.as_ref().map_or_else(
|
||||||
|
|| Err(String::from("There is no TextSvg for this song")),
|
||||||
if text_svg.handle.is_none() {
|
|text_svg| {
|
||||||
return Err(String::from("There is no handle in this song's TextSvg"));
|
if text_svg.handle.is_none() {
|
||||||
};
|
return Err(String::from(
|
||||||
|
"There is no handle in this song's TextSvg",
|
||||||
text_svg.path.as_ref().map_or(Err(String::from("There is no path in this song's TextSvg")), |path| {
|
));
|
||||||
if path.exists() {
|
|
||||||
let mut path = path.clone();
|
|
||||||
if path.metadata().unwrap().len() < 20000 {
|
|
||||||
return Err(String::from("SVG text is too small, maybe the svg didn't generate properly"))
|
|
||||||
}
|
|
||||||
if path.pop() && path == cache_dir {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(String::from("The path of the TextSvg isn't in the load directory"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(String::from("The path in this TextSvg doesn't exist"))
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
text_svg.path.as_ref().map_or_else(
|
||||||
|
|| {
|
||||||
|
Err(String::from(
|
||||||
|
"There is no path in this song's TextSvg",
|
||||||
|
))
|
||||||
|
},
|
||||||
|
|path| {
|
||||||
|
if path.exists() {
|
||||||
|
test_size_and_cache(path.clone())
|
||||||
|
} else {
|
||||||
|
Err(String::from(
|
||||||
|
"The path in this TextSvg doesn't exist",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -454,20 +424,20 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks to make sure all paths in slides and items point to cache_dir
|
// checks to make sure all paths in slides and items point to cache_dir
|
||||||
fn find_paths(items: &Vec<ServiceItem>) -> bool {
|
fn find_paths(items: &[ServiceItem]) -> bool {
|
||||||
let cache_dir = cache_dir();
|
let cache_dir = cache_dir();
|
||||||
items.iter().all(|item| {
|
items.iter().all(|item| {
|
||||||
match &item.kind {
|
match &item.kind {
|
||||||
ServiceItemKind::Song(song) => {
|
ServiceItemKind::Song(song) => {
|
||||||
if let Some(bg) = &song.background {
|
if let Some(bg) = &song.background
|
||||||
if !bg.path.starts_with(&cache_dir) {
|
&& !bg.path.starts_with(&cache_dir)
|
||||||
return false;
|
{
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(audio) = &song.audio {
|
if let Some(audio) = &song.audio
|
||||||
if !audio.starts_with(&cache_dir) {
|
&& !audio.starts_with(&cache_dir)
|
||||||
return false;
|
{
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ServiceItemKind::Video(video) => {
|
ServiceItemKind::Video(video) => {
|
||||||
|
|
@ -491,9 +461,10 @@ mod test {
|
||||||
if !slide.background().path.starts_with(&cache_dir) {
|
if !slide.background().path.starts_with(&cache_dir) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if !slide.audio().map_or(true, |audio| {
|
if !slide
|
||||||
audio.starts_with(&cache_dir)
|
.audio()
|
||||||
}) {
|
.is_none_or(|audio| audio.starts_with(&cache_dir))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -502,7 +473,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cache_dir() -> PathBuf {
|
fn cache_dir() -> PathBuf {
|
||||||
let mut cache_dir = dirs::cache_dir().unwrap();
|
let mut cache_dir = dirs::cache_dir().expect("");
|
||||||
cache_dir.push("lumina");
|
cache_dir.push("lumina");
|
||||||
cache_dir.push("cached_save_files");
|
cache_dir.push("cached_save_files");
|
||||||
cache_dir.push("test");
|
cache_dir.push("test");
|
||||||
|
|
@ -513,22 +484,18 @@ mod test {
|
||||||
fn test_save() {
|
fn test_save() {
|
||||||
let path = PathBuf::from("./test.pres");
|
let path = PathBuf::from("./test.pres");
|
||||||
let list = get_items();
|
let list = get_items();
|
||||||
match save(list, &path, true) {
|
match save(&Arc::new(list), &path, true) {
|
||||||
Ok(_) => {
|
Ok(()) => {
|
||||||
assert!(path.is_file());
|
assert!(path.is_file());
|
||||||
let Ok(file) = fs::File::open(path) else {
|
let Ok(file) = fs::File::open(path) else {
|
||||||
return assert!(false, "couldn't open file");
|
panic!("couldn't open file");
|
||||||
};
|
};
|
||||||
let Ok(size) = file.metadata().map(|data| data.len())
|
let Ok(size) = file.metadata().map(|data| data.len()) else {
|
||||||
else {
|
panic!("couldn't get file metadata");
|
||||||
return assert!(
|
|
||||||
false,
|
|
||||||
"couldn't get file metadata"
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
assert!(size > 0);
|
assert!(size > 0);
|
||||||
}
|
}
|
||||||
Err(e) => assert!(false, "{e}"),
|
Err(e) => panic!("{e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,30 @@
|
||||||
|
use crate::core::model::{Sort, SortDirection};
|
||||||
use crate::{Background, Slide, SlideBuilder, TextAlignment};
|
use crate::{Background, Slide, SlideBuilder, TextAlignment};
|
||||||
|
|
||||||
use super::{
|
use super::content::Content;
|
||||||
content::Content,
|
use super::kinds::ServiceItemKind;
|
||||||
kinds::ServiceItemKind,
|
use super::model::{LibraryKind, Model};
|
||||||
model::{LibraryKind, Model},
|
use super::service_items::ServiceTrait;
|
||||||
service_items::ServiceTrait,
|
|
||||||
};
|
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use itertools::Itertools;
|
||||||
|
use miette::{IntoDiagnostic, Result, miette};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::types::chrono::{DateTime, Local};
|
||||||
Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
|
use sqlx::{AssertSqlSafe, SqliteConnection, SqlitePool, query, query_as};
|
||||||
query, query_as,
|
use std::mem::replace;
|
||||||
};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tracing::{debug, error};
|
use std::sync::Arc;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub accessed_at: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PathBuf> for Image {
|
impl From<PathBuf> for Image {
|
||||||
|
|
@ -37,6 +39,8 @@ impl From<PathBuf> for Image {
|
||||||
id: 0,
|
id: 0,
|
||||||
title,
|
title,
|
||||||
path: value.canonicalize().unwrap_or(value),
|
path: value.canonicalize().unwrap_or(value),
|
||||||
|
created_at: Local::now(),
|
||||||
|
accessed_at: Local::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,22 +97,19 @@ impl From<&Value> for Image {
|
||||||
fn from(value: &Value) -> Self {
|
fn from(value: &Value) -> Self {
|
||||||
match value {
|
match value {
|
||||||
Value::List(list) => {
|
Value::List(list) => {
|
||||||
let path = if let Some(path_pos) =
|
let path = if let Some(path_pos) = list
|
||||||
list.iter().position(|v| {
|
.iter()
|
||||||
v == &Value::Keyword(Keyword::from("source"))
|
.position(|v| v == &Value::Keyword(Keyword::from("source")))
|
||||||
}) {
|
{
|
||||||
let pos = path_pos + 1;
|
let pos = path_pos + 1;
|
||||||
list.get(pos)
|
list.get(pos).map(|p| PathBuf::from(String::from(p)))
|
||||||
.map(|p| PathBuf::from(String::from(p)))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let title = path.clone().map(|p| {
|
let title = path.clone().map(|p| {
|
||||||
let path =
|
let path = p.to_str().unwrap_or_default().to_string();
|
||||||
p.to_str().unwrap_or_default().to_string();
|
let title = path.rsplit_once('/').unwrap_or_default().1;
|
||||||
let title =
|
|
||||||
path.rsplit_once('/').unwrap_or_default().1;
|
|
||||||
title.to_string()
|
title.to_string()
|
||||||
});
|
});
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -133,10 +134,7 @@ impl ServiceTrait for Image {
|
||||||
|
|
||||||
fn to_slides(&self) -> Result<Vec<Slide>> {
|
fn to_slides(&self) -> Result<Vec<Slide>> {
|
||||||
let slide = SlideBuilder::new()
|
let slide = SlideBuilder::new()
|
||||||
.background(
|
.background(Background::try_from(self.path.clone()).into_diagnostic()?)
|
||||||
Background::try_from(self.path.clone())
|
|
||||||
.into_diagnostic()?,
|
|
||||||
)
|
|
||||||
.text("")
|
.text("")
|
||||||
.audio("")
|
.audio("")
|
||||||
.font("")
|
.font("")
|
||||||
|
|
@ -156,10 +154,11 @@ impl ServiceTrait for Image {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model<Image> {
|
impl Model<Image> {
|
||||||
pub async fn new_image_model(db: &mut SqlitePool) -> Self {
|
pub async fn new_image_model(db: Arc<SqlitePool>) -> Self {
|
||||||
let mut model = Self {
|
let mut model = Self {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Image,
|
kind: LibraryKind::Image,
|
||||||
|
sorting_method: Sort::AccessTime(SortDirection::Descending),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut db = db.acquire().await.expect("probs");
|
let mut db = db.acquire().await.expect("probs");
|
||||||
|
|
@ -171,7 +170,7 @@ impl Model<Image> {
|
||||||
pub async fn load_from_db(&mut self, db: &mut SqliteConnection) {
|
pub async fn load_from_db(&mut self, db: &mut SqliteConnection) {
|
||||||
let result = query_as!(
|
let result = query_as!(
|
||||||
Image,
|
Image,
|
||||||
r#"SELECT title as "title!", file_path as "path!", id as "id: i32" from images"#
|
r#"SELECT title as "title!", file_path as "path!", id as "id: i32", accessed_at as "accessed_at!: DateTime<Local>", created_at as "created_at!: DateTime<Local>" from images"#
|
||||||
)
|
)
|
||||||
.fetch_all(db)
|
.fetch_all(db)
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -182,83 +181,150 @@ impl Model<Image> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!("There was an error in converting images: {e}");
|
||||||
"There was an error in converting images: {e}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sort(&mut self) {
|
||||||
|
match self.sorting_method {
|
||||||
|
Sort::AccessTime(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.accessed_at.cmp(&a.accessed_at))
|
||||||
|
}
|
||||||
|
Sort::AccessTime(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.accessed_at.cmp(&b.accessed_at))
|
||||||
|
}
|
||||||
|
Sort::Title(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.title.cmp(&a.title))
|
||||||
|
}
|
||||||
|
Sort::Title(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.title.cmp(&b.title))
|
||||||
|
}
|
||||||
|
Sort::CreatedTime(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.created_at.cmp(&a.created_at))
|
||||||
|
}
|
||||||
|
Sort::CreatedTime(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.created_at.cmp(&b.created_at))
|
||||||
|
}
|
||||||
|
Sort::Secondary(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.path.cmp(&a.path))
|
||||||
|
}
|
||||||
|
Sort::Secondary(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.path.cmp(&b.path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sort(mut self, method: Sort) -> Self {
|
||||||
|
self.sorting_method = method;
|
||||||
|
self.sort();
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_from_db(
|
pub async fn remove_images(
|
||||||
db: PoolConnection<Sqlite>,
|
db: Arc<SqlitePool>,
|
||||||
id: i32,
|
images: Vec<Image>,
|
||||||
) -> Result<()> {
|
ids: Vec<i32>,
|
||||||
query!("DELETE FROM images WHERE id = $1", id)
|
) -> Result<Vec<Image>> {
|
||||||
.execute(&mut db.detach())
|
let images = images
|
||||||
|
.into_iter()
|
||||||
|
.filter(|current_image| !ids.contains(¤t_image.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let delete = format!(
|
||||||
|
"DELETE FROM images WHERE id IN ({:})",
|
||||||
|
ids.iter().map(ToString::to_string).join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
query(AssertSqlSafe(delete))
|
||||||
|
.execute(&*db)
|
||||||
.await
|
.await
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
.map(|_| ())
|
.map(|_| images)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_image_to_db(
|
pub async fn remove_image(
|
||||||
|
db: Arc<SqlitePool>,
|
||||||
|
mut images: Vec<Image>,
|
||||||
|
id: i32,
|
||||||
|
) -> Result<Vec<Image>> {
|
||||||
|
query!("DELETE FROM images WHERE id = $1", id)
|
||||||
|
.execute(&*db)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.map(|_| ())?;
|
||||||
|
let index = images
|
||||||
|
.iter()
|
||||||
|
.position(|current_image| current_image.id == id)
|
||||||
|
.ok_or_else(|| miette!("Could not find image in model"))?;
|
||||||
|
images.remove(index);
|
||||||
|
Ok(images)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_image(
|
||||||
|
new_images: Vec<Image>,
|
||||||
|
mut current_images: Vec<Image>,
|
||||||
|
db: Arc<SqlitePool>,
|
||||||
|
) -> Result<Vec<Image>> {
|
||||||
|
for image in new_images {
|
||||||
|
let path = image
|
||||||
|
.path
|
||||||
|
.to_str()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
query!(
|
||||||
|
r#"INSERT INTO images (title, file_path) VALUES ($1, $2)"#,
|
||||||
|
image.title,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
.execute(&*db)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
current_images.push(image);
|
||||||
|
}
|
||||||
|
Ok(current_images)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_image(
|
||||||
image: Image,
|
image: Image,
|
||||||
db: PoolConnection<Sqlite>,
|
mut images: Vec<Image>,
|
||||||
) -> Result<()> {
|
db: Arc<SqlitePool>,
|
||||||
|
) -> Result<Vec<Image>> {
|
||||||
let path = image
|
let path = image
|
||||||
.path
|
.path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mut db = db.detach();
|
|
||||||
query!(
|
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(
|
|
||||||
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();
|
|
||||||
debug!(?image, "should be been updated");
|
|
||||||
let result = query!(
|
|
||||||
r#"UPDATE images SET title = $2, file_path = $3 WHERE id = $1"#,
|
r#"UPDATE images SET title = $2, file_path = $3 WHERE id = $1"#,
|
||||||
image.id,
|
image.id,
|
||||||
image.title,
|
image.title,
|
||||||
path,
|
path,
|
||||||
)
|
)
|
||||||
.execute(&mut db)
|
.execute(&*db)
|
||||||
.await.into_diagnostic();
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
match result {
|
let current_image = images
|
||||||
Ok(_) => {
|
.iter()
|
||||||
debug!("should have been updated");
|
.position(|current_image| current_image.id == image.id)
|
||||||
Ok(())
|
.ok_or_else(|| miette!("Could not find image in model"))
|
||||||
}
|
.map(|index| {
|
||||||
Err(e) => {
|
images
|
||||||
error! {?e};
|
.get_mut(index)
|
||||||
Err(e)
|
.expect("We should have this image already")
|
||||||
}
|
})?;
|
||||||
}
|
|
||||||
|
let _ = replace(current_image, image);
|
||||||
|
Ok(images)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_image_from_db(
|
pub async fn get_from_db(database_id: i32, db: &mut SqliteConnection) -> Result<Image> {
|
||||||
database_id: i32,
|
query_as!(Image, r#"SELECT title as "title!", file_path as "path!", id as "id: i32", accessed_at as "accessed_at!: DateTime<Local>", created_at as "created_at!: DateTime<Local>" from images where id = ?"#, database_id).fetch_one(db).await.into_diagnostic()
|
||||||
db: &mut SqliteConnection,
|
|
||||||
) -> Result<Image> {
|
|
||||||
query_as!(Image, r#"SELECT title as "title!", file_path as "path!", id as "id: i32" from images where id = ?"#, database_id).fetch_one(db).await.into_diagnostic()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -269,9 +335,7 @@ mod test {
|
||||||
fn test_image(title: String) -> Image {
|
fn test_image(title: String) -> Image {
|
||||||
Image {
|
Image {
|
||||||
title,
|
title,
|
||||||
path: PathBuf::from(
|
path: PathBuf::from("/home/chris/pics/memes/no-i-dont-think.gif"),
|
||||||
"/home/chris/pics/memes/no-i-dont-think.gif",
|
|
||||||
),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,14 +345,20 @@ mod test {
|
||||||
let mut image_model: Model<Image> = Model {
|
let mut image_model: Model<Image> = Model {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Image,
|
kind: LibraryKind::Image,
|
||||||
|
sorting_method: Sort::AccessTime(SortDirection::Descending),
|
||||||
};
|
};
|
||||||
let mut db = add_db().await.unwrap().acquire().await.unwrap();
|
let mut db = add_db()
|
||||||
|
.await
|
||||||
|
.expect("Error getting db")
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.expect("");
|
||||||
image_model.load_from_db(&mut db).await;
|
image_model.load_from_db(&mut db).await;
|
||||||
if let Some(image) = image_model.find(|i| i.id == 23) {
|
if let Some(image) = image_model.find(|i| i.id == 23) {
|
||||||
let test_image = test_image("no-i-dont-think.gif".into());
|
let test_image = test_image("no-i-dont-think.gif".into());
|
||||||
assert_eq!(test_image.title, image.title);
|
assert_eq!(test_image.title, image.title);
|
||||||
} else {
|
} else {
|
||||||
assert!(false);
|
panic!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,25 +368,18 @@ mod test {
|
||||||
let mut image_model: Model<Image> = Model {
|
let mut image_model: Model<Image> = Model {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Image,
|
kind: LibraryKind::Image,
|
||||||
|
sorting_method: Sort::AccessTime(SortDirection::Descending),
|
||||||
};
|
};
|
||||||
let result = image_model.add_item(image.clone());
|
let result = image_model.add_item(image.clone());
|
||||||
let new_image = test_image("A newer image".into());
|
let new_image = test_image("A newer image".into());
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(()) => {
|
||||||
assert_eq!(
|
assert_eq!(&image, image_model.find(|i| i.id == 0).expect(""));
|
||||||
&image,
|
assert_ne!(&new_image, image_model.find(|i| i.id == 0).expect(""));
|
||||||
image_model.find(|i| i.id == 0).unwrap()
|
}
|
||||||
);
|
Err(e) => {
|
||||||
assert_ne!(
|
panic!("There was an error adding the image: {e:?}",)
|
||||||
&new_image,
|
|
||||||
image_model.find(|i| i.id == 0).unwrap()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(e) => assert!(
|
|
||||||
false,
|
|
||||||
"There was an error adding the image: {:?}",
|
|
||||||
e
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
use std::{error::Error, fmt::Display, path::PathBuf};
|
use std::error::Error;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::Slide;
|
||||||
Slide,
|
use crate::core::content::Content;
|
||||||
core::{content::Content, service_items::ServiceItem},
|
use crate::core::service_items::ServiceItem;
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::images::Image;
|
||||||
images::Image, presentations::Presentation, songs::Song,
|
use super::presentations::Presentation;
|
||||||
videos::Video,
|
use super::songs::Song;
|
||||||
};
|
use super::videos::Video;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ServiceItemKind {
|
pub enum ServiceItemKind {
|
||||||
|
|
@ -28,18 +29,10 @@ impl TryFrom<PathBuf> for ServiceItemKind {
|
||||||
let ext = path
|
let ext = path
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|ext| ext.to_str())
|
.and_then(|ext| ext.to_str())
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| miette::miette!("There isn't an extension on this file"))?;
|
||||||
miette::miette!(
|
|
||||||
"There isn't an extension on this file"
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
match ext {
|
match ext {
|
||||||
"png" | "jpg" | "jpeg" => {
|
"png" | "jpg" | "jpeg" => Ok(Self::Image(Image::from(path))),
|
||||||
Ok(Self::Image(Image::from(path)))
|
"mp4" | "mkv" | "webm" => Ok(Self::Video(Video::from(path))),
|
||||||
}
|
|
||||||
"mp4" | "mkv" | "webm" => {
|
|
||||||
Ok(Self::Video(Video::from(path)))
|
|
||||||
}
|
|
||||||
"pdf" => Ok(Self::Presentation(Presentation::from(path))),
|
"pdf" => Ok(Self::Presentation(Presentation::from(path))),
|
||||||
_ => Err(miette::miette!("Unknown item")),
|
_ => Err(miette::miette!("Unknown item")),
|
||||||
}
|
}
|
||||||
|
|
@ -52,9 +45,7 @@ impl ServiceItemKind {
|
||||||
Self::Song(song) => song.title.clone(),
|
Self::Song(song) => song.title.clone(),
|
||||||
Self::Video(video) => video.title.clone(),
|
Self::Video(video) => video.title.clone(),
|
||||||
Self::Image(image) => image.title.clone(),
|
Self::Image(image) => image.title.clone(),
|
||||||
Self::Presentation(presentation) => {
|
Self::Presentation(presentation) => presentation.title.clone(),
|
||||||
presentation.title.clone()
|
|
||||||
}
|
|
||||||
Self::Content(_slide) => todo!(),
|
Self::Content(_slide) => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,9 +55,7 @@ impl ServiceItemKind {
|
||||||
Self::Song(song) => song.to_service_item(),
|
Self::Song(song) => song.to_service_item(),
|
||||||
Self::Video(video) => video.to_service_item(),
|
Self::Video(video) => video.to_service_item(),
|
||||||
Self::Image(image) => image.to_service_item(),
|
Self::Image(image) => image.to_service_item(),
|
||||||
Self::Presentation(presentation) => {
|
Self::Presentation(presentation) => presentation.to_service_item(),
|
||||||
presentation.to_service_item()
|
|
||||||
}
|
|
||||||
Self::Content(_slide) => {
|
Self::Content(_slide) => {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
@ -111,9 +100,7 @@ impl From<ServiceItemKind> for String {
|
||||||
ServiceItemKind::Song(_) => "song".to_owned(),
|
ServiceItemKind::Song(_) => "song".to_owned(),
|
||||||
ServiceItemKind::Video(_) => "video".to_owned(),
|
ServiceItemKind::Video(_) => "video".to_owned(),
|
||||||
ServiceItemKind::Image(_) => "image".to_owned(),
|
ServiceItemKind::Image(_) => "image".to_owned(),
|
||||||
ServiceItemKind::Presentation(_) => {
|
ServiceItemKind::Presentation(_) => "presentation".to_owned(),
|
||||||
"presentation".to_owned()
|
|
||||||
}
|
|
||||||
ServiceItemKind::Content(_) => "content".to_owned(),
|
ServiceItemKind::Content(_) => "content".to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,10 +114,7 @@ pub enum ParseError {
|
||||||
impl Error for ParseError {}
|
impl Error for ParseError {}
|
||||||
|
|
||||||
impl Display for ParseError {
|
impl Display for ParseError {
|
||||||
fn fmt(
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
&self,
|
|
||||||
f: &mut std::fmt::Formatter<'_>,
|
|
||||||
) -> std::fmt::Result {
|
|
||||||
let message = match self {
|
let message = match self {
|
||||||
Self::UnknownType => {
|
Self::UnknownType => {
|
||||||
"The type does not exist. It needs to be one of 'song', 'video', 'image', 'presentation', or 'content'"
|
"The type does not exist. It needs to be one of 'song', 'video', 'image', 'presentation', or 'content'"
|
||||||
|
|
@ -144,6 +128,6 @@ impl Display for ParseError {
|
||||||
mod test {
|
mod test {
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_kinds() {
|
pub fn test_kinds() {
|
||||||
assert_eq!(true, true)
|
assert_eq!(true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@ pub mod song_search;
|
||||||
pub mod songs;
|
pub mod songs;
|
||||||
pub mod thumbnail;
|
pub mod thumbnail;
|
||||||
pub mod videos;
|
pub mod videos;
|
||||||
|
pub mod ytdl;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use std::{borrow::Cow, fs, mem::replace, path::PathBuf};
|
use std::borrow::Cow;
|
||||||
|
use std::fs;
|
||||||
|
use std::mem::replace;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
use cosmic::iced::clipboard::mime::{AllowedMimeTypes, AsMimeTypes};
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
use miette::{IntoDiagnostic, Result, miette};
|
||||||
|
|
@ -10,11 +13,10 @@ use tracing::debug;
|
||||||
pub struct Model<T> {
|
pub struct Model<T> {
|
||||||
pub items: Vec<T>,
|
pub items: Vec<T>,
|
||||||
pub kind: LibraryKind,
|
pub kind: LibraryKind,
|
||||||
|
pub sorting_method: Sort,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, Serialize, Deserialize)]
|
||||||
Debug, Clone, PartialEq, Eq, Copy, Hash, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub enum LibraryKind {
|
pub enum LibraryKind {
|
||||||
Song,
|
Song,
|
||||||
Video,
|
Video,
|
||||||
|
|
@ -22,9 +24,21 @@ pub enum LibraryKind {
|
||||||
Presentation,
|
Presentation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, Eq, PartialEq, Copy, Serialize, Deserialize)]
|
||||||
Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize,
|
pub enum Sort {
|
||||||
)]
|
AccessTime(SortDirection),
|
||||||
|
CreatedTime(SortDirection),
|
||||||
|
Title(SortDirection),
|
||||||
|
Secondary(SortDirection), // This can be author or file name
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Copy, Serialize, Deserialize)]
|
||||||
|
pub enum SortDirection {
|
||||||
|
Ascending,
|
||||||
|
Descending,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct KindWrapper(pub (LibraryKind, i32));
|
pub struct KindWrapper(pub (LibraryKind, i32));
|
||||||
|
|
||||||
impl From<PathBuf> for LibraryKind {
|
impl From<PathBuf> for LibraryKind {
|
||||||
|
|
@ -36,14 +50,10 @@ impl From<PathBuf> for LibraryKind {
|
||||||
impl TryFrom<(Vec<u8>, String)> for KindWrapper {
|
impl TryFrom<(Vec<u8>, String)> for KindWrapper {
|
||||||
type Error = miette::Error;
|
type Error = miette::Error;
|
||||||
|
|
||||||
fn try_from(
|
fn try_from(value: (Vec<u8>, String)) -> std::result::Result<Self, Self::Error> {
|
||||||
value: (Vec<u8>, String),
|
|
||||||
) -> std::result::Result<Self, Self::Error> {
|
|
||||||
let (data, mime) = value;
|
let (data, mime) = value;
|
||||||
match mime.as_str() {
|
match mime.as_str() {
|
||||||
"application/service-item" => {
|
"application/service-item" => ron::de::from_bytes(&data).into_diagnostic(),
|
||||||
ron::de::from_bytes(&data).into_diagnostic()
|
|
||||||
}
|
|
||||||
_ => Err(miette!("Wrong mime type: {mime}")),
|
_ => Err(miette!("Wrong mime type: {mime}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,10 +71,7 @@ impl AsMimeTypes for KindWrapper {
|
||||||
Cow::from(vec!["application/service-item".to_string()])
|
Cow::from(vec!["application/service-item".to_string()])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_bytes(
|
fn as_bytes(&self, mime_type: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||||
&self,
|
|
||||||
mime_type: &str,
|
|
||||||
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
|
||||||
debug!(?self);
|
debug!(?self);
|
||||||
debug!(mime_type);
|
debug!(mime_type);
|
||||||
let ron = ron::ser::to_string(self).ok()?;
|
let ron = ron::ser::to_string(self).ok()?;
|
||||||
|
|
@ -83,37 +90,41 @@ impl<T> Model<T> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_item(&mut self, item: T, index: i32) -> Result<()> {
|
pub fn update_item<P>(&mut self, item: T, predicate: P) -> Result<()>
|
||||||
|
where
|
||||||
|
P: Fn(&T) -> bool,
|
||||||
|
{
|
||||||
self.items
|
self.items
|
||||||
.get_mut(
|
.iter()
|
||||||
usize::try_from(index)
|
.position(predicate)
|
||||||
.expect("Shouldn't be negative"),
|
.ok_or_else(|| miette!("Item cannot be found"))
|
||||||
)
|
.map(|index| {
|
||||||
.map_or_else(
|
self.items
|
||||||
|| {
|
.get_mut(index)
|
||||||
Err(miette!(
|
.expect("Since we found position this should always exist")
|
||||||
"Item doesn't exist in model. Id was {index}"
|
})
|
||||||
))
|
.map(|current_item| {
|
||||||
},
|
let _old_item = replace(current_item, item);
|
||||||
|current_item| {
|
})
|
||||||
let _old_item = replace(current_item, item);
|
|
||||||
Ok(())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_item(&mut self, index: i32) -> Result<()> {
|
pub fn remove_item<P>(&mut self, predicate: P) -> Result<()>
|
||||||
self.items.remove(
|
where
|
||||||
usize::try_from(index).expect("Shouldn't be negative"),
|
P: Fn(&T) -> bool,
|
||||||
);
|
{
|
||||||
Ok(())
|
self.items
|
||||||
|
.iter()
|
||||||
|
.position(predicate)
|
||||||
|
.ok_or_else(|| miette!("Item cannot be found"))
|
||||||
|
.map(|index| {
|
||||||
|
self.items.remove(index);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_item(&self, index: i32) -> Option<&T> {
|
pub fn get_item(&self, index: i32) -> Option<&T> {
|
||||||
self.items.get(
|
self.items
|
||||||
usize::try_from(index).expect("shouldn't be negative"),
|
.get(usize::try_from(index).expect("shouldn't be negative"))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find<P>(&self, f: P) -> Option<&T>
|
pub fn find<P>(&self, f: P) -> Option<&T>
|
||||||
|
|
@ -123,11 +134,8 @@ impl<T> Model<T> {
|
||||||
self.items.iter().find(f)
|
self.items.iter().find(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_item(&mut self, item: T, index: i32) -> Result<()> {
|
pub fn insert_item(&mut self, item: T, index: usize) -> Result<()> {
|
||||||
self.items.insert(
|
self.items.insert(index, item);
|
||||||
usize::try_from(index).expect("Shouldn't be negative"),
|
|
||||||
item,
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -144,8 +152,7 @@ impl<T> Model<T> {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub async fn get_db() -> SqliteConnection {
|
pub async fn get_db() -> SqliteConnection {
|
||||||
let mut data = dirs::data_local_dir()
|
let mut data = dirs::data_local_dir().expect("Should be able to find a data dir");
|
||||||
.expect("Should be able to find a data dir");
|
|
||||||
data.push("lumina");
|
data.push("lumina");
|
||||||
let _ = fs::create_dir_all(&data);
|
let _ = fs::create_dir_all(&data);
|
||||||
data.push("library-db.sqlite3");
|
data.push("library-db.sqlite3");
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
use cosmic::widget::image::Handle;
|
use cosmic::widget::image::Handle;
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use itertools::Itertools;
|
||||||
|
use miette::{IntoDiagnostic, Result, miette};
|
||||||
use mupdf::{Colorspace, Document, Matrix};
|
use mupdf::{Colorspace, Document, Matrix};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::prelude::FromRow;
|
||||||
Row, Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
|
use sqlx::sqlite::SqliteRow;
|
||||||
prelude::FromRow, query, sqlite::SqliteRow,
|
use sqlx::types::chrono::{DateTime, Local};
|
||||||
};
|
use sqlx::{AssertSqlSafe, Row, SqliteConnection, SqlitePool, query};
|
||||||
|
use std::mem::replace;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
use crate::core::model::{Sort, SortDirection};
|
||||||
use crate::{Background, Slide, SlideBuilder, TextAlignment};
|
use crate::{Background, Slide, SlideBuilder, TextAlignment};
|
||||||
|
|
||||||
use super::{
|
use super::content::Content;
|
||||||
content::Content,
|
use super::kinds::ServiceItemKind;
|
||||||
kinds::ServiceItemKind,
|
use super::model::{LibraryKind, Model};
|
||||||
model::{LibraryKind, Model},
|
use super::service_items::ServiceTrait;
|
||||||
service_items::ServiceTrait,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub enum PresKind {
|
pub enum PresKind {
|
||||||
Html,
|
Html,
|
||||||
Pdf {
|
Pdf {
|
||||||
|
|
@ -38,6 +38,10 @@ pub struct Presentation {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub kind: PresKind,
|
pub kind: PresKind,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub accessed_at: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for Presentation {}
|
impl Eq for Presentation {}
|
||||||
|
|
@ -65,7 +69,7 @@ impl From<PathBuf> for Presentation {
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
{
|
{
|
||||||
"pdf" => Document::open(&value.as_path()).map_or(
|
"pdf" => Document::open(&value.to_str().unwrap_or_default()).map_or(
|
||||||
PresKind::Pdf {
|
PresKind::Pdf {
|
||||||
starting_index: 0,
|
starting_index: 0,
|
||||||
ending_index: 0,
|
ending_index: 0,
|
||||||
|
|
@ -91,6 +95,8 @@ impl From<PathBuf> for Presentation {
|
||||||
title,
|
title,
|
||||||
path: value.canonicalize().unwrap_or(value),
|
path: value.canonicalize().unwrap_or(value),
|
||||||
kind,
|
kind,
|
||||||
|
created_at: Local::now(),
|
||||||
|
accessed_at: Local::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,20 +153,19 @@ impl From<&Value> for Presentation {
|
||||||
fn from(value: &Value) -> Self {
|
fn from(value: &Value) -> Self {
|
||||||
match value {
|
match value {
|
||||||
Value::List(list) => {
|
Value::List(list) => {
|
||||||
let path = if let Some(path_pos) =
|
let path = if let Some(path_pos) = list
|
||||||
list.iter().position(|v| {
|
.iter()
|
||||||
v == &Value::Keyword(Keyword::from("source"))
|
.position(|v| v == &Value::Keyword(Keyword::from("source")))
|
||||||
}) {
|
{
|
||||||
let pos = path_pos + 1;
|
let pos = path_pos + 1;
|
||||||
list.get(pos)
|
list.get(pos).map(|p| PathBuf::from(String::from(p)))
|
||||||
.map(|p| PathBuf::from(String::from(p)))
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let title = path.clone().map(|p| {
|
let title = path
|
||||||
p.to_str().unwrap_or_default().to_string()
|
.clone()
|
||||||
});
|
.map(|p| p.to_str().unwrap_or_default().to_string());
|
||||||
Self {
|
Self {
|
||||||
title: title.unwrap_or_default(),
|
title: title.unwrap_or_default(),
|
||||||
path: path.unwrap_or_default(),
|
path: path.unwrap_or_default(),
|
||||||
|
|
@ -188,14 +193,11 @@ impl ServiceTrait for Presentation {
|
||||||
ending_index,
|
ending_index,
|
||||||
} = self.kind
|
} = self.kind
|
||||||
else {
|
else {
|
||||||
return Err(miette::miette!(
|
return Err(miette::miette!("This is not a pdf presentation"));
|
||||||
"This is not a pdf presentation"
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
let background = Background::try_from(self.path.clone())
|
let background = Background::try_from(self.path.clone()).into_diagnostic()?;
|
||||||
.into_diagnostic()?;
|
|
||||||
debug!(?background);
|
debug!(?background);
|
||||||
let document = Document::open(background.path.as_path())
|
let document = Document::open(background.path.to_str().unwrap_or_default())
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
debug!(?document);
|
debug!(?document);
|
||||||
let pages = document.pages().into_diagnostic()?;
|
let pages = document.pages().into_diagnostic()?;
|
||||||
|
|
@ -203,8 +205,7 @@ impl ServiceTrait for Presentation {
|
||||||
let pages: Vec<Handle> = pages
|
let pages: Vec<Handle> = pages
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(index, page)| {
|
.filter_map(|(index, page)| {
|
||||||
let index = i32::try_from(index)
|
let index = i32::try_from(index).expect("Shouldn't be that high");
|
||||||
.expect("Shouldn't be that high");
|
|
||||||
|
|
||||||
if index < starting_index || index > ending_index {
|
if index < starting_index || index > ending_index {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -232,10 +233,7 @@ impl ServiceTrait for Presentation {
|
||||||
let mut slides: Vec<Slide> = vec![];
|
let mut slides: Vec<Slide> = vec![];
|
||||||
for (index, page) in pages.into_iter().enumerate() {
|
for (index, page) in pages.into_iter().enumerate() {
|
||||||
let slide = SlideBuilder::new()
|
let slide = SlideBuilder::new()
|
||||||
.background(
|
.background(Background::try_from(self.path.clone()).into_diagnostic()?)
|
||||||
Background::try_from(self.path.clone())
|
|
||||||
.into_diagnostic()?,
|
|
||||||
)
|
|
||||||
.text("")
|
.text("")
|
||||||
.audio("")
|
.audio("")
|
||||||
.font("")
|
.font("")
|
||||||
|
|
@ -244,10 +242,7 @@ impl ServiceTrait for Presentation {
|
||||||
.video_loop(false)
|
.video_loop(false)
|
||||||
.video_start_time(0.0)
|
.video_start_time(0.0)
|
||||||
.video_end_time(0.0)
|
.video_end_time(0.0)
|
||||||
.pdf_index(
|
.pdf_index(u32::try_from(index).expect("Shouldn't get that high"))
|
||||||
u32::try_from(index)
|
|
||||||
.expect("Shouldn't get that high"),
|
|
||||||
)
|
|
||||||
.pdf_page(page)
|
.pdf_page(page)
|
||||||
.build()?;
|
.build()?;
|
||||||
slides.push(slide);
|
slides.push(slide);
|
||||||
|
|
@ -293,27 +288,28 @@ impl FromRow<'_, SqliteRow> for Presentation {
|
||||||
ending_index: row.try_get(5)?,
|
ending_index: row.try_get(5)?,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created_at: Local::now(),
|
||||||
|
accessed_at: Local::now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model<Presentation> {
|
impl Model<Presentation> {
|
||||||
pub async fn new_presentation_model(db: &mut SqlitePool) -> Self {
|
pub async fn new_presentation_model(db: Arc<SqlitePool>) -> Self {
|
||||||
let mut model = Self {
|
let mut model = Self {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Presentation,
|
kind: LibraryKind::Presentation,
|
||||||
|
sorting_method: Sort::AccessTime(SortDirection::Descending),
|
||||||
};
|
};
|
||||||
let mut db = db.acquire().await.expect("probs");
|
model.load_from_db(db).await;
|
||||||
|
|
||||||
model.load_from_db(&mut db).await;
|
|
||||||
model
|
model
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_from_db(&mut self, db: &mut SqliteConnection) {
|
pub async fn load_from_db(&mut self, db: Arc<SqlitePool>) {
|
||||||
let result = query!(
|
let result = query!(
|
||||||
r#"SELECT id as "id: i32", title, file_path as "path", html, starting_index, ending_index from presentations"#
|
r#"SELECT id as "id: i32", title, file_path as "path", html, starting_index, ending_index, accessed_at as "accessed_at!: DateTime<Local>", created_at as "created_at!: DateTime<Local>" from presentations"#
|
||||||
)
|
)
|
||||||
.fetch_all(db)
|
.fetch_all(&*db)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
|
|
@ -325,28 +321,19 @@ impl Model<Presentation> {
|
||||||
path: presentation.path.clone().into(),
|
path: presentation.path.clone().into(),
|
||||||
kind: if presentation.html {
|
kind: if presentation.html {
|
||||||
PresKind::Html
|
PresKind::Html
|
||||||
} else if let (
|
} else if let (Some(starting_index), Some(ending_index)) =
|
||||||
Some(starting_index),
|
(presentation.starting_index, presentation.ending_index)
|
||||||
Some(ending_index),
|
{
|
||||||
) = (
|
|
||||||
presentation.starting_index,
|
|
||||||
presentation.ending_index,
|
|
||||||
) {
|
|
||||||
PresKind::Pdf {
|
PresKind::Pdf {
|
||||||
starting_index: i32::try_from(
|
starting_index: i32::try_from(starting_index)
|
||||||
starting_index,
|
.expect("Shouldn't get that high"),
|
||||||
)
|
ending_index: i32::try_from(ending_index)
|
||||||
.expect("Shouldn't get that high"),
|
.expect("Shouldn't get that high"),
|
||||||
ending_index: i32::try_from(
|
|
||||||
ending_index,
|
|
||||||
)
|
|
||||||
.expect("Shouldn't get that high"),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let path =
|
let path = PathBuf::from(presentation.path);
|
||||||
PathBuf::from(presentation.path);
|
|
||||||
|
|
||||||
Document::open(path.as_path()).map_or(
|
Document::open(path.to_str().unwrap_or_default()).map_or(
|
||||||
PresKind::Generic,
|
PresKind::Generic,
|
||||||
|document| {
|
|document| {
|
||||||
document.page_count().map_or(
|
document.page_count().map_or(
|
||||||
|
|
@ -355,8 +342,7 @@ impl Model<Presentation> {
|
||||||
ending_index: 0,
|
ending_index: 0,
|
||||||
},
|
},
|
||||||
|count| {
|
|count| {
|
||||||
let ending_index =
|
let ending_index = count - 1;
|
||||||
count - 1;
|
|
||||||
PresKind::Pdf {
|
PresKind::Pdf {
|
||||||
starting_index: 0,
|
starting_index: 0,
|
||||||
ending_index,
|
ending_index,
|
||||||
|
|
@ -366,130 +352,153 @@ impl Model<Presentation> {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
created_at: presentation.created_at,
|
||||||
|
accessed_at: presentation.accessed_at,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!(
|
Err(e) => error!("There was an error in converting presentations: {e}"),
|
||||||
"There was an error in converting presentations: {e}"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sort(&mut self) {
|
||||||
|
match self.sorting_method {
|
||||||
|
Sort::AccessTime(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.accessed_at.cmp(&a.accessed_at))
|
||||||
|
}
|
||||||
|
Sort::AccessTime(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.accessed_at.cmp(&b.accessed_at))
|
||||||
|
}
|
||||||
|
Sort::Title(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.title.cmp(&a.title))
|
||||||
|
}
|
||||||
|
Sort::Title(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.title.cmp(&b.title))
|
||||||
|
}
|
||||||
|
Sort::CreatedTime(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.created_at.cmp(&a.created_at))
|
||||||
|
}
|
||||||
|
Sort::CreatedTime(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.created_at.cmp(&b.created_at))
|
||||||
|
}
|
||||||
|
Sort::Secondary(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.path.cmp(&a.path))
|
||||||
|
}
|
||||||
|
Sort::Secondary(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.path.cmp(&b.path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sort(mut self, method: Sort) -> Self {
|
||||||
|
self.sorting_method = method;
|
||||||
|
self.sort();
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_from_db(
|
pub async fn remove_presentations(
|
||||||
db: PoolConnection<Sqlite>,
|
db: Arc<SqlitePool>,
|
||||||
id: i32,
|
presentations: Vec<Presentation>,
|
||||||
) -> Result<()> {
|
ids: Vec<i32>,
|
||||||
query!("DELETE FROM presentations WHERE id = $1", id)
|
) -> Result<Vec<Presentation>> {
|
||||||
.execute(&mut db.detach())
|
let presentations = presentations
|
||||||
|
.into_iter()
|
||||||
|
.filter(|current_presentation| !ids.contains(¤t_presentation.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let delete = format!(
|
||||||
|
"DELETE FROM presentations WHERE id IN ({:})",
|
||||||
|
ids.iter().map(ToString::to_string).join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
query(AssertSqlSafe(delete))
|
||||||
|
.execute(&*db)
|
||||||
.await
|
.await
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
.map(|_| ())
|
.map(|_| presentations)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_presentation_to_db(
|
pub async fn remove_presentation(
|
||||||
|
db: Arc<SqlitePool>,
|
||||||
|
mut presentations: Vec<Presentation>,
|
||||||
|
id: i32,
|
||||||
|
) -> Result<Vec<Presentation>> {
|
||||||
|
query!("DELETE FROM presentations WHERE id = $1", id)
|
||||||
|
.execute(&*db)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.map(|_| ())?;
|
||||||
|
|
||||||
|
let index = presentations
|
||||||
|
.iter()
|
||||||
|
.position(|current_presentation| current_presentation.id == id)
|
||||||
|
.ok_or_else(|| miette!("Could not find presentation in model"))?;
|
||||||
|
presentations.remove(index);
|
||||||
|
Ok(presentations)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_presentation(
|
||||||
|
new_presentations: Vec<Presentation>,
|
||||||
|
mut current_presentations: Vec<Presentation>,
|
||||||
|
db: Arc<SqlitePool>,
|
||||||
|
) -> Result<Vec<Presentation>> {
|
||||||
|
for presentation in new_presentations {
|
||||||
|
let path = presentation
|
||||||
|
.path
|
||||||
|
.to_str()
|
||||||
|
.map(std::string::ToString::to_string)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let html = presentation.kind == PresKind::Html;
|
||||||
|
let (starting_index, ending_index) = if let PresKind::Pdf {
|
||||||
|
starting_index,
|
||||||
|
ending_index,
|
||||||
|
} = presentation.kind
|
||||||
|
{
|
||||||
|
(starting_index, ending_index)
|
||||||
|
} else {
|
||||||
|
(0, 0)
|
||||||
|
};
|
||||||
|
query!(
|
||||||
|
r#"INSERT INTO presentations (title, file_path, html, starting_index, ending_index) VALUES ($1, $2, $3, $4, $5)"#,
|
||||||
|
presentation.title,
|
||||||
|
path,
|
||||||
|
html,
|
||||||
|
starting_index,
|
||||||
|
ending_index
|
||||||
|
)
|
||||||
|
.execute(&*db)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
current_presentations.push(presentation);
|
||||||
|
}
|
||||||
|
Ok(current_presentations)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_presentation(
|
||||||
presentation: Presentation,
|
presentation: Presentation,
|
||||||
db: PoolConnection<Sqlite>,
|
mut presentations: Vec<Presentation>,
|
||||||
) -> Result<()> {
|
db: Arc<SqlitePool>,
|
||||||
|
) -> Result<Vec<Presentation>> {
|
||||||
let path = presentation
|
let path = presentation
|
||||||
.path
|
.path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let html = presentation.kind == PresKind::Html;
|
let html = presentation.kind == PresKind::Html;
|
||||||
let mut db = db.detach();
|
|
||||||
let (starting_index, ending_index) = if let PresKind::Pdf {
|
|
||||||
starting_index,
|
|
||||||
ending_index,
|
|
||||||
} = presentation.kind
|
|
||||||
{
|
|
||||||
(starting_index, ending_index)
|
|
||||||
} else {
|
|
||||||
(0, 0)
|
|
||||||
};
|
|
||||||
query!(
|
|
||||||
r#"INSERT INTO presentations (title, file_path, html, starting_index, ending_index) VALUES ($1, $2, $3, $4, $5)"#,
|
|
||||||
presentation.title,
|
|
||||||
path,
|
|
||||||
html,
|
|
||||||
starting_index,
|
|
||||||
ending_index
|
|
||||||
)
|
|
||||||
.execute(&mut db)
|
|
||||||
.await
|
|
||||||
.into_diagnostic()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_presentation_in_db(
|
|
||||||
presentation: Presentation,
|
|
||||||
db: PoolConnection<Sqlite>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let path = presentation
|
|
||||||
.path
|
|
||||||
.to_str()
|
|
||||||
.map(std::string::ToString::to_string)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let html = presentation.kind == PresKind::Html;
|
|
||||||
let mut db = db.detach();
|
|
||||||
let (starting_index, ending_index) = if let PresKind::Pdf {
|
let (starting_index, ending_index) = if let PresKind::Pdf {
|
||||||
starting_index: s_index,
|
starting_index: s_index,
|
||||||
ending_index: e_index,
|
ending_index: e_index,
|
||||||
} =
|
} = presentation.get_kind()
|
||||||
presentation.get_kind()
|
|
||||||
{
|
{
|
||||||
(*s_index, *e_index)
|
(*s_index, *e_index)
|
||||||
} else {
|
} else {
|
||||||
(0, 0)
|
(0, 0)
|
||||||
};
|
};
|
||||||
debug!(starting_index, ending_index);
|
debug!(starting_index, ending_index);
|
||||||
let id = presentation.id;
|
|
||||||
if let Err(e) =
|
|
||||||
query!("SELECT id FROM presentations where id = $1", id)
|
|
||||||
.fetch_one(&mut db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if let Ok(ids) = query!("SELECT id FROM presentations")
|
|
||||||
.fetch_all(&mut db)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let Some(mut max) = ids.iter().map(|r| r.id).max() else {
|
|
||||||
return Err(miette::miette!("cannot find max id"));
|
|
||||||
};
|
|
||||||
debug!(
|
|
||||||
?e,
|
|
||||||
"Presentation not found adding a new presentation"
|
|
||||||
);
|
|
||||||
max += 1;
|
|
||||||
let result = query!(
|
|
||||||
r#"INSERT into presentations VALUES($1, $2, $3, $4, $5, $6)"#,
|
|
||||||
max,
|
|
||||||
presentation.title,
|
|
||||||
path,
|
|
||||||
html,
|
|
||||||
starting_index,
|
|
||||||
ending_index,
|
|
||||||
)
|
|
||||||
.execute(&mut db)
|
|
||||||
.await
|
|
||||||
.into_diagnostic();
|
|
||||||
|
|
||||||
return match result {
|
query!(
|
||||||
Ok(_) => {
|
|
||||||
debug!("presentation should have been added");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error! {?e};
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return Err(miette::miette!("cannot find ids"));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(?presentation, "should be been updated");
|
|
||||||
let result = query!(
|
|
||||||
r#"UPDATE presentations SET title = $2, file_path = $3, html = $4, starting_index = $5, ending_index = $6 WHERE id = $1"#,
|
r#"UPDATE presentations SET title = $2, file_path = $3, html = $4, starting_index = $5, ending_index = $6 WHERE id = $1"#,
|
||||||
presentation.id,
|
presentation.id,
|
||||||
presentation.title,
|
presentation.title,
|
||||||
|
|
@ -498,26 +507,28 @@ pub async fn update_presentation_in_db(
|
||||||
starting_index,
|
starting_index,
|
||||||
ending_index
|
ending_index
|
||||||
)
|
)
|
||||||
.execute(&mut db)
|
.execute(&*db)
|
||||||
.await.into_diagnostic();
|
.await.into_diagnostic()?;
|
||||||
|
|
||||||
match result {
|
let current_presentation = presentations
|
||||||
Ok(_) => {
|
.iter()
|
||||||
debug!("should have been updated");
|
.position(|current_presentation| current_presentation.id == presentation.id)
|
||||||
Ok(())
|
.ok_or_else(|| miette!("Could not find presentation in model"))
|
||||||
}
|
.map(|index| {
|
||||||
Err(e) => {
|
presentations
|
||||||
error! {?e};
|
.get_mut(index)
|
||||||
Err(e)
|
.expect("We should have this presentation already")
|
||||||
}
|
})?;
|
||||||
}
|
|
||||||
|
let _ = replace(current_presentation, presentation);
|
||||||
|
Ok(presentations)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_presentation_from_db(
|
pub async fn get_presentation_from_db(
|
||||||
database_id: i32,
|
database_id: i32,
|
||||||
db: &mut SqliteConnection,
|
db: &mut SqliteConnection,
|
||||||
) -> Result<Presentation> {
|
) -> Result<Presentation> {
|
||||||
let row = query(r#"SELECT id as "id: i32", title, file_path as "path", html from presentations where id = $1"#).bind(database_id).fetch_one(db).await.into_diagnostic()?;
|
let row = query(r#"SELECT id as "id: i32", title, file_path as "path", html, accessed_at as "accessed_at!: DateTime<Local>", created_at as "created_at!: DateTime<Local>" from presentations where id = $1"#).bind(database_id).fetch_one(db).await.into_diagnostic()?;
|
||||||
Presentation::from_row(&row).into_diagnostic()
|
Presentation::from_row(&row).into_diagnostic()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -535,17 +546,19 @@ mod test {
|
||||||
starting_index: 0,
|
starting_index: 0,
|
||||||
ending_index: 67,
|
ending_index: 67,
|
||||||
},
|
},
|
||||||
|
created_at: Local::now(),
|
||||||
|
accessed_at: Local::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_pres() {
|
pub fn test_pres() {
|
||||||
let pres = Presentation::new();
|
let pres = Presentation::new();
|
||||||
assert_eq!(pres.get_kind(), &PresKind::Generic)
|
assert_eq!(pres.get_kind(), &PresKind::Generic);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_db() -> Result<SqlitePool> {
|
async fn add_db() -> Result<SqlitePool> {
|
||||||
let mut db_url = String::from("sqlite://./test.db");
|
let db_url = String::from("sqlite://./test.db");
|
||||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
SqlitePool::connect(&db_url).await.into_diagnostic()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,16 +567,15 @@ mod test {
|
||||||
let mut presentation_model: Model<Presentation> = Model {
|
let mut presentation_model: Model<Presentation> = Model {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Presentation,
|
kind: LibraryKind::Presentation,
|
||||||
|
sorting_method: Sort::AccessTime(SortDirection::Descending),
|
||||||
};
|
};
|
||||||
let mut db = add_db().await.unwrap().acquire().await.unwrap();
|
let db = Arc::new(add_db().await.expect("Getting db error"));
|
||||||
presentation_model.load_from_db(&mut db).await;
|
presentation_model.load_from_db(db).await;
|
||||||
if let Some(presentation) =
|
if let Some(presentation) = presentation_model.find(|p| p.id == 4) {
|
||||||
presentation_model.find(|p| p.id == 4)
|
|
||||||
{
|
|
||||||
let test_presentation = test_presentation();
|
let test_presentation = test_presentation();
|
||||||
assert_eq!(&test_presentation, presentation);
|
assert_eq!(&test_presentation, presentation);
|
||||||
} else {
|
} else {
|
||||||
assert!(false);
|
panic!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,7 @@ impl Ord for ServiceItem {
|
||||||
impl TryFrom<(Vec<u8>, String)> for ServiceItem {
|
impl TryFrom<(Vec<u8>, String)> for ServiceItem {
|
||||||
type Error = miette::Error;
|
type Error = miette::Error;
|
||||||
|
|
||||||
fn try_from(
|
fn try_from(value: (Vec<u8>, String)) -> std::result::Result<Self, Self::Error> {
|
||||||
value: (Vec<u8>, String),
|
|
||||||
) -> std::result::Result<Self, Self::Error> {
|
|
||||||
let (data, mime) = value;
|
let (data, mime) = value;
|
||||||
debug!(?mime);
|
debug!(?mime);
|
||||||
ron::de::from_bytes(&data).into_diagnostic()
|
ron::de::from_bytes(&data).into_diagnostic()
|
||||||
|
|
@ -70,10 +68,7 @@ impl AsMimeTypes for ServiceItem {
|
||||||
Cow::from(vec!["application/service-item".to_string()])
|
Cow::from(vec!["application/service-item".to_string()])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_bytes(
|
fn as_bytes(&self, mime_type: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||||
&self,
|
|
||||||
mime_type: &str,
|
|
||||||
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
|
||||||
debug!(?self);
|
debug!(?self);
|
||||||
debug!(mime_type);
|
debug!(mime_type);
|
||||||
let ron = ron::ser::to_string(self).ok()?;
|
let ron = ron::ser::to_string(self).ok()?;
|
||||||
|
|
@ -89,18 +84,10 @@ impl TryFrom<PathBuf> for ServiceItem {
|
||||||
let ext = path
|
let ext = path
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|ext| ext.to_str())
|
.and_then(|ext| ext.to_str())
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| miette::miette!("There isn't an extension on this file"))?;
|
||||||
miette::miette!(
|
|
||||||
"There isn't an extension on this file"
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
match ext {
|
match ext {
|
||||||
"png" | "jpg" | "jpeg" => {
|
"png" | "jpg" | "jpeg" => Ok(Self::from(&Image::from(path))),
|
||||||
Ok(Self::from(&Image::from(path)))
|
"mp4" | "mkv" | "webm" => Ok(Self::from(&Video::from(path))),
|
||||||
}
|
|
||||||
"mp4" | "mkv" | "webm" => {
|
|
||||||
Ok(Self::from(&Video::from(path)))
|
|
||||||
}
|
|
||||||
_ => Err(miette!("Unkown service item")),
|
_ => Err(miette!("Unkown service item")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,9 +99,7 @@ impl From<&ServiceItem> for Value {
|
||||||
ServiceItemKind::Song(song) => Self::from(song),
|
ServiceItemKind::Song(song) => Self::from(song),
|
||||||
ServiceItemKind::Video(video) => Self::from(video),
|
ServiceItemKind::Video(video) => Self::from(video),
|
||||||
ServiceItemKind::Image(image) => Self::from(image),
|
ServiceItemKind::Image(image) => Self::from(image),
|
||||||
ServiceItemKind::Presentation(presentation) => {
|
ServiceItemKind::Presentation(presentation) => Self::from(presentation),
|
||||||
Self::from(presentation)
|
|
||||||
}
|
|
||||||
ServiceItemKind::Content(slide) => Self::from(slide),
|
ServiceItemKind::Content(slide) => Self::from(slide),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,12 +115,8 @@ impl ServiceItem {
|
||||||
ServiceItemKind::Song(song) => song.to_slides(),
|
ServiceItemKind::Song(song) => song.to_slides(),
|
||||||
ServiceItemKind::Video(video) => video.to_slides(),
|
ServiceItemKind::Video(video) => video.to_slides(),
|
||||||
ServiceItemKind::Image(image) => image.to_slides(),
|
ServiceItemKind::Image(image) => image.to_slides(),
|
||||||
ServiceItemKind::Presentation(presentation) => {
|
ServiceItemKind::Presentation(presentation) => presentation.to_slides(),
|
||||||
presentation.to_slides()
|
ServiceItemKind::Content(slide) => Ok(vec![slide.clone()]),
|
||||||
}
|
|
||||||
ServiceItemKind::Content(slide) => {
|
|
||||||
Ok(vec![slide.clone()])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,70 +158,44 @@ impl From<&Value> for ServiceItem {
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
.map_or_else(|| 1, |pos| pos + 1);
|
.map_or_else(|| 1, |pos| pos + 1);
|
||||||
if let Some(_content) =
|
if let Some(_content) = list.iter().position(|v| match v {
|
||||||
list.iter().position(|v| match v {
|
Value::List(list)
|
||||||
Value::List(list)
|
if list.iter().next()
|
||||||
if list.iter().next()
|
== Some(&Value::Symbol(Symbol("text".into()))) =>
|
||||||
== Some(&Value::Symbol(
|
{
|
||||||
Symbol("text".into()),
|
list.iter().next().is_some()
|
||||||
)) =>
|
}
|
||||||
{
|
_ => false,
|
||||||
list.iter().next().is_some()
|
}) {
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
let slide = Slide::from(value);
|
let slide = Slide::from(value);
|
||||||
let title = slide.text();
|
let title = slide.text();
|
||||||
Self {
|
Self {
|
||||||
id: 0,
|
id: 0,
|
||||||
title,
|
title,
|
||||||
database_id: 0,
|
database_id: 0,
|
||||||
kind: ServiceItemKind::Content(
|
kind: ServiceItemKind::Content(slide.clone()),
|
||||||
slide.clone(),
|
|
||||||
),
|
|
||||||
slides: vec![slide],
|
slides: vec![slide],
|
||||||
}
|
}
|
||||||
} else if let Some(background) =
|
} else if let Some(background) = list.get(background_pos) {
|
||||||
list.get(background_pos)
|
|
||||||
{
|
|
||||||
if let Value::List(item) = background {
|
if let Value::List(item) = background {
|
||||||
match &item[0] {
|
match &item[0] {
|
||||||
Value::Symbol(Symbol(s))
|
Value::Symbol(Symbol(s)) if s == "image" => {
|
||||||
if s == "image" =>
|
Self::from(&Image::from(background))
|
||||||
{
|
|
||||||
Self::from(&Image::from(
|
|
||||||
background,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
Value::Symbol(Symbol(s))
|
Value::Symbol(Symbol(s)) if s == "video" => {
|
||||||
if s == "video" =>
|
Self::from(&Video::from(background))
|
||||||
{
|
|
||||||
Self::from(&Video::from(
|
|
||||||
background,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
Value::Symbol(Symbol(s))
|
Value::Symbol(Symbol(s)) if s == "presentation" => {
|
||||||
if s == "presentation" =>
|
Self::from(&Presentation::from(background))
|
||||||
{
|
|
||||||
Self::from(&Presentation::from(
|
|
||||||
background,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!("There is no background here: {:?}", background);
|
||||||
"There is no background here: {:?}",
|
|
||||||
background
|
|
||||||
);
|
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!("There is no background here: {:?}", background_pos);
|
||||||
"There is no background here: {:?}",
|
|
||||||
background_pos
|
|
||||||
);
|
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -346,9 +301,7 @@ impl From<&Presentation> for ServiceItem {
|
||||||
fn from(presentation: &Presentation) -> Self {
|
fn from(presentation: &Presentation) -> Self {
|
||||||
match presentation.to_slides() {
|
match presentation.to_slides() {
|
||||||
Ok(slides) => Self {
|
Ok(slides) => Self {
|
||||||
kind: ServiceItemKind::Presentation(
|
kind: ServiceItemKind::Presentation(presentation.clone()),
|
||||||
presentation.clone(),
|
|
||||||
),
|
|
||||||
database_id: presentation.id,
|
database_id: presentation.id,
|
||||||
title: presentation.title.clone(),
|
title: presentation.title.clone(),
|
||||||
slides,
|
slides,
|
||||||
|
|
@ -357,9 +310,7 @@ impl From<&Presentation> for ServiceItem {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(?e);
|
error!(?e);
|
||||||
Self {
|
Self {
|
||||||
kind: ServiceItemKind::Presentation(
|
kind: ServiceItemKind::Presentation(presentation.clone()),
|
||||||
presentation.clone(),
|
|
||||||
),
|
|
||||||
database_id: presentation.id,
|
database_id: presentation.id,
|
||||||
title: presentation.title.clone(),
|
title: presentation.title.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -410,10 +361,7 @@ impl Clone for Box<dyn ServiceTrait> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Box<dyn ServiceTrait> {
|
impl std::fmt::Debug for Box<dyn ServiceTrait> {
|
||||||
fn fmt(
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
&self,
|
|
||||||
f: &mut std::fmt::Formatter<'_>,
|
|
||||||
) -> Result<(), std::fmt::Error> {
|
|
||||||
write!(f, "{}: {}", self.id(), self.title())
|
write!(f, "{}: {}", self.id(), self.title())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -426,6 +374,7 @@ mod test {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use sqlx::types::chrono::Local;
|
||||||
|
|
||||||
fn test_song() -> Song {
|
fn test_song() -> Song {
|
||||||
Song {
|
Song {
|
||||||
|
|
@ -442,6 +391,8 @@ mod test {
|
||||||
"~/docs/notes/lessons/20240327T133649--12-isaiah-and-jesus__lesson_project_tfc.html",
|
"~/docs/notes/lessons/20240327T133649--12-isaiah-and-jesus__lesson_project_tfc.html",
|
||||||
),
|
),
|
||||||
kind: PresKind::Html,
|
kind: PresKind::Html,
|
||||||
|
created_at: Local::now(),
|
||||||
|
accessed_at: Local::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -453,14 +404,8 @@ mod test {
|
||||||
let pres_item = ServiceItem::from(&pres);
|
let pres_item = ServiceItem::from(&pres);
|
||||||
let mut service_model = Service::default();
|
let mut service_model = Service::default();
|
||||||
service_model.add_item(&song);
|
service_model.add_item(&song);
|
||||||
assert_eq!(
|
assert_eq!(ServiceItemKind::Song(song), service_model.items[0].kind);
|
||||||
ServiceItemKind::Song(song),
|
assert_eq!(ServiceItemKind::Presentation(pres), pres_item.kind);
|
||||||
service_model.items[0].kind
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ServiceItemKind::Presentation(pres),
|
|
||||||
pres_item.kind
|
|
||||||
);
|
|
||||||
assert_eq!(service_item, service_model.items[0]);
|
assert_eq!(service_item, service_model.items[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
||||||
cosmic_config::{
|
use cosmic::cosmic_config::{self, CosmicConfigEntry};
|
||||||
self, CosmicConfigEntry,
|
use cosmic::theme;
|
||||||
cosmic_config_derive::CosmicConfigEntry,
|
|
||||||
},
|
|
||||||
theme,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::VecDeque, path::PathBuf};
|
use std::collections::VecDeque;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::core::model::Sort;
|
||||||
|
|
||||||
pub const SETTINGS_VERSION: u64 = 1;
|
pub const SETTINGS_VERSION: u64 = 1;
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize,
|
|
||||||
)]
|
|
||||||
pub enum AppTheme {
|
pub enum AppTheme {
|
||||||
Dark,
|
Dark,
|
||||||
Light,
|
Light,
|
||||||
|
|
@ -31,19 +28,16 @@ impl AppTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
Clone,
|
|
||||||
CosmicConfigEntry,
|
|
||||||
Debug,
|
|
||||||
Deserialize,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
Serialize,
|
|
||||||
)]
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub app_theme: AppTheme,
|
pub app_theme: AppTheme,
|
||||||
pub obs_url: Option<url::Url>,
|
pub obs_url: Option<url::Url>,
|
||||||
|
pub genius_token: Option<String>,
|
||||||
|
pub song_sort: Option<Sort>,
|
||||||
|
pub image_sort: Option<Sort>,
|
||||||
|
pub video_sort: Option<Sort>,
|
||||||
|
pub presentation_sort: Option<Sort>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
|
|
@ -51,19 +45,17 @@ impl Default for Settings {
|
||||||
Self {
|
Self {
|
||||||
app_theme: AppTheme::System,
|
app_theme: AppTheme::System,
|
||||||
obs_url: None,
|
obs_url: None,
|
||||||
|
genius_token: None,
|
||||||
|
song_sort: None,
|
||||||
|
image_sort: None,
|
||||||
|
video_sort: None,
|
||||||
|
presentation_sort: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone,
|
Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize, Default,
|
||||||
CosmicConfigEntry,
|
|
||||||
Debug,
|
|
||||||
Deserialize,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
Serialize,
|
|
||||||
Default,
|
|
||||||
)]
|
)]
|
||||||
pub struct PersistentState {
|
pub struct PersistentState {
|
||||||
pub recent_files: VecDeque<PathBuf>,
|
pub recent_files: VecDeque<PathBuf>,
|
||||||
|
|
|
||||||
208
src/core/slide.rs
Normal file → Executable file
|
|
@ -1,26 +1,28 @@
|
||||||
#![allow(clippy::similar_names, unused)]
|
#![allow(clippy::similar_names, unused)]
|
||||||
|
use cosmic::iced::Size;
|
||||||
|
use cosmic::iced::core::image::Allocation;
|
||||||
use cosmic::widget::image::Handle;
|
use cosmic::widget::image::Handle;
|
||||||
// use cosmic::dialog::ashpd::url::Url;
|
// use cosmic::dialog::ashpd::url::Url;
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use iced_video_player::Video;
|
use iced_video_player::Video;
|
||||||
|
use image::EncodableLayout;
|
||||||
use miette::{Result, miette};
|
use miette::{Result, miette};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::fmt::Display;
|
||||||
fmt::Display,
|
use std::path::{Path, PathBuf};
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::ui::gst_video;
|
||||||
use crate::ui::text_svg::{Color, Font, Shadow, Stroke, TextSvg};
|
use crate::ui::text_svg::{Color, Font, Shadow, Stroke, TextSvg};
|
||||||
|
|
||||||
use super::songs::Song;
|
use super::songs::Song;
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Slide {
|
pub struct Slide {
|
||||||
id: i32,
|
id: i32,
|
||||||
pub(crate) background: Background,
|
pub(crate) background: Background,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub(crate) thumbnail: Option<Allocation>,
|
||||||
text: String,
|
text: String,
|
||||||
font: Option<Font>,
|
font: Option<Font>,
|
||||||
font_size: i32,
|
font_size: i32,
|
||||||
|
|
@ -38,9 +40,7 @@ pub struct Slide {
|
||||||
pdf_page: Option<Handle>,
|
pdf_page: Option<Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub enum BackgroundKind {
|
pub enum BackgroundKind {
|
||||||
#[default]
|
#[default]
|
||||||
Image,
|
Image,
|
||||||
|
|
@ -49,17 +49,7 @@ pub enum BackgroundKind {
|
||||||
Html,
|
Html,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||||
Clone,
|
|
||||||
Copy,
|
|
||||||
Debug,
|
|
||||||
Default,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
Hash,
|
|
||||||
)]
|
|
||||||
pub enum TextAlignment {
|
pub enum TextAlignment {
|
||||||
TopLeft,
|
TopLeft,
|
||||||
TopCenter,
|
TopCenter,
|
||||||
|
|
@ -89,20 +79,20 @@ impl From<&Value> for TextAlignment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Background {
|
pub struct Background {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub kind: BackgroundKind,
|
pub kind: BackgroundKind,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub image_handle: Option<Handle>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub image_allocation: Option<Allocation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&Background> for Video {
|
impl TryFrom<&Background> for Video {
|
||||||
type Error = ParseError;
|
type Error = ParseError;
|
||||||
|
|
||||||
fn try_from(
|
fn try_from(value: &Background) -> std::result::Result<Self, Self::Error> {
|
||||||
value: &Background,
|
|
||||||
) -> std::result::Result<Self, Self::Error> {
|
|
||||||
Self::new(
|
Self::new(
|
||||||
&url::Url::from_file_path(value.path.clone())
|
&url::Url::from_file_path(value.path.clone())
|
||||||
.map_err(|()| ParseError::BackgroundNotVideo)?,
|
.map_err(|()| ParseError::BackgroundNotVideo)?,
|
||||||
|
|
@ -114,14 +104,17 @@ impl TryFrom<&Background> for Video {
|
||||||
impl TryFrom<Background> for Video {
|
impl TryFrom<Background> for Video {
|
||||||
type Error = ParseError;
|
type Error = ParseError;
|
||||||
|
|
||||||
fn try_from(
|
fn try_from(value: Background) -> std::result::Result<Self, Self::Error> {
|
||||||
value: Background,
|
let url = &url::Url::from_file_path(value.path)
|
||||||
) -> std::result::Result<Self, Self::Error> {
|
.map_err(|()| ParseError::BackgroundNotVideo)?;
|
||||||
Self::new(
|
|
||||||
&url::Url::from_file_path(value.path)
|
let settings = gst_video::VideoSettings {
|
||||||
.map_err(|()| ParseError::BackgroundNotVideo)?,
|
mute: true,
|
||||||
)
|
framerate: 30,
|
||||||
.map_err(|_| ParseError::BackgroundNotVideo)
|
appsink_name: "lumina_video".to_string(),
|
||||||
|
};
|
||||||
|
gst_video::create_video(url, &settings)
|
||||||
|
.map_err(|_| ParseError::BackgroundNotVideo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,10 +129,7 @@ impl TryFrom<PathBuf> for Background {
|
||||||
type Error = ParseError;
|
type Error = ParseError;
|
||||||
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
||||||
let path = if path.starts_with("~") {
|
let path = if path.starts_with("~") {
|
||||||
let path = path
|
let path = path.to_str().expect("Should have a string").to_string();
|
||||||
.to_str()
|
|
||||||
.expect("Should have a string")
|
|
||||||
.to_string();
|
|
||||||
let path = path.trim_start_matches("file://");
|
let path = path.trim_start_matches("file://");
|
||||||
let home = dirs::home_dir()
|
let home = dirs::home_dir()
|
||||||
.expect("We should have a home directory")
|
.expect("We should have a home directory")
|
||||||
|
|
@ -161,20 +151,28 @@ impl TryFrom<PathBuf> for Background {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
match extension {
|
match extension {
|
||||||
"jpeg" | "jpg" | "png" | "webp" => Ok(Self {
|
"jpeg" | "jpg" | "png" | "webp" => Ok(Self {
|
||||||
path: value,
|
path: value.clone(),
|
||||||
kind: BackgroundKind::Image,
|
kind: BackgroundKind::Image,
|
||||||
|
image_handle: Some(value.into()),
|
||||||
|
image_allocation: None,
|
||||||
}),
|
}),
|
||||||
"mp4" | "mkv" | "webm" => Ok(Self {
|
"mp4" | "mkv" | "webm" => Ok(Self {
|
||||||
path: value,
|
path: value,
|
||||||
kind: BackgroundKind::Video,
|
kind: BackgroundKind::Video,
|
||||||
|
image_handle: None,
|
||||||
|
image_allocation: None,
|
||||||
}),
|
}),
|
||||||
"pdf" => Ok(Self {
|
"pdf" => Ok(Self {
|
||||||
path: value,
|
path: value,
|
||||||
kind: BackgroundKind::Pdf,
|
kind: BackgroundKind::Pdf,
|
||||||
|
image_handle: None,
|
||||||
|
image_allocation: None,
|
||||||
}),
|
}),
|
||||||
"html" => Ok(Self {
|
"html" => Ok(Self {
|
||||||
path: value,
|
path: value,
|
||||||
kind: BackgroundKind::Html,
|
kind: BackgroundKind::Html,
|
||||||
|
image_handle: None,
|
||||||
|
image_allocation: None,
|
||||||
}),
|
}),
|
||||||
_ => Err(ParseError::NonBackgroundFile),
|
_ => Err(ParseError::NonBackgroundFile),
|
||||||
}
|
}
|
||||||
|
|
@ -230,21 +228,12 @@ pub enum ParseError {
|
||||||
impl std::error::Error for ParseError {}
|
impl std::error::Error for ParseError {}
|
||||||
|
|
||||||
impl Display for ParseError {
|
impl Display for ParseError {
|
||||||
fn fmt(
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
&self,
|
|
||||||
f: &mut std::fmt::Formatter<'_>,
|
|
||||||
) -> std::fmt::Result {
|
|
||||||
let message = match self {
|
let message = match self {
|
||||||
Self::NonBackgroundFile => {
|
Self::NonBackgroundFile => "The file is not a recognized image or video type",
|
||||||
"The file is not a recognized image or video type"
|
|
||||||
}
|
|
||||||
Self::DoesNotExist => "This file doesn't exist",
|
Self::DoesNotExist => "This file doesn't exist",
|
||||||
Self::CannotCanonicalize => {
|
Self::CannotCanonicalize => "Could not canonicalize this file",
|
||||||
"Could not canonicalize this file"
|
Self::BackgroundNotVideo => "This background isn't a video",
|
||||||
}
|
|
||||||
Self::BackgroundNotVideo => {
|
|
||||||
"This background isn't a video"
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
write!(f, "Error: {message}")
|
write!(f, "Error: {message}")
|
||||||
}
|
}
|
||||||
|
|
@ -391,9 +380,7 @@ impl Slide {
|
||||||
.background(song.background.unwrap_or_default())
|
.background(song.background.unwrap_or_default())
|
||||||
.font(song.font.unwrap_or_default())
|
.font(song.font.unwrap_or_default())
|
||||||
.font_size(song.font_size.unwrap_or_default())
|
.font_size(song.font_size.unwrap_or_default())
|
||||||
.text_alignment(
|
.text_alignment(song.text_alignment.unwrap_or_default())
|
||||||
song.text_alignment.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
.audio(song.audio.unwrap_or_default())
|
.audio(song.audio.unwrap_or_default())
|
||||||
.video_loop(true)
|
.video_loop(true)
|
||||||
.video_start_time(0.0)
|
.video_start_time(0.0)
|
||||||
|
|
@ -437,10 +424,10 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
||||||
const DEFAULT_TEXT_LOCATION: usize = 0;
|
const DEFAULT_TEXT_LOCATION: usize = 0;
|
||||||
|
|
||||||
let mut slide = SlideBuilder::new();
|
let mut slide = SlideBuilder::new();
|
||||||
let background_position = if let Some(background) =
|
let background_position = if let Some(background) = lisp
|
||||||
lisp.iter().position(|v| {
|
.iter()
|
||||||
v == &Value::Keyword(Keyword::from("background"))
|
.position(|v| v == &Value::Keyword(Keyword::from("background")))
|
||||||
}) {
|
{
|
||||||
background + 1
|
background + 1
|
||||||
} else {
|
} else {
|
||||||
DEFAULT_BACKGROUND_LOCATION
|
DEFAULT_BACKGROUND_LOCATION
|
||||||
|
|
@ -454,8 +441,7 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
||||||
|
|
||||||
let text_position = lisp.iter().position(|v| match v {
|
let text_position = lisp.iter().position(|v| match v {
|
||||||
Value::List(vec) => {
|
Value::List(vec) => {
|
||||||
vec[DEFAULT_TEXT_LOCATION]
|
vec[DEFAULT_TEXT_LOCATION] == Value::Symbol(Symbol::from("text"))
|
||||||
== Value::Symbol(Symbol::from("text"))
|
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
});
|
});
|
||||||
|
|
@ -500,14 +486,11 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
||||||
fn lisp_to_font_size(lisp: &Value) -> i32 {
|
fn lisp_to_font_size(lisp: &Value) -> i32 {
|
||||||
match lisp {
|
match lisp {
|
||||||
Value::List(list) => {
|
Value::List(list) => {
|
||||||
if let Some(font_size_position) =
|
if let Some(font_size_position) = list
|
||||||
list.iter().position(|v| {
|
.iter()
|
||||||
v == &Value::Keyword(Keyword::from("font-size"))
|
.position(|v| v == &Value::Keyword(Keyword::from("font-size")))
|
||||||
})
|
|
||||||
{
|
{
|
||||||
if let Some(font_size_value) =
|
if let Some(font_size_value) = list.get(font_size_position + 1) {
|
||||||
list.get(font_size_position + 1)
|
|
||||||
{
|
|
||||||
font_size_value.into()
|
font_size_value.into()
|
||||||
} else {
|
} else {
|
||||||
50
|
50
|
||||||
|
|
@ -534,9 +517,10 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
||||||
match lisp {
|
match lisp {
|
||||||
Value::List(list) => {
|
Value::List(list) => {
|
||||||
let _kind = list[0].clone();
|
let _kind = list[0].clone();
|
||||||
if let Some(source) = list.iter().position(|v| {
|
if let Some(source) = list
|
||||||
v == &Value::Keyword(Keyword::from("source"))
|
.iter()
|
||||||
}) {
|
.position(|v| v == &Value::Keyword(Keyword::from("source")))
|
||||||
|
{
|
||||||
let source = &list[source + 1];
|
let source = &list[source + 1];
|
||||||
match source {
|
match source {
|
||||||
Value::String(s) => {
|
Value::String(s) => {
|
||||||
|
|
@ -554,9 +538,7 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
||||||
match Background::try_from(s.as_str()) {
|
match Background::try_from(s.as_str()) {
|
||||||
Ok(background) => background,
|
Ok(background) => background,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!("Couldn't load background: {e}");
|
||||||
"Couldn't load background: {e}"
|
|
||||||
);
|
|
||||||
Background::default()
|
Background::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -564,9 +546,7 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
||||||
match Background::try_from(s.as_str()) {
|
match Background::try_from(s.as_str()) {
|
||||||
Ok(background) => background,
|
Ok(background) => background,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!("Couldn't load background: {e}");
|
||||||
"Couldn't load background: {e}"
|
|
||||||
);
|
|
||||||
Background::default()
|
Background::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -582,9 +562,7 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct SlideBuilder {
|
pub struct SlideBuilder {
|
||||||
background: Option<Background>,
|
background: Option<Background>,
|
||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
|
|
@ -619,10 +597,7 @@ impl SlideBuilder {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn background(
|
pub(crate) fn background(mut self, background: Background) -> Self {
|
||||||
mut self,
|
|
||||||
background: Background,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.background.insert(background);
|
let _ = self.background.insert(background);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -632,10 +607,7 @@ impl SlideBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn text_color(
|
pub(crate) fn text_color(mut self, text_color: impl Into<Color>) -> Self {
|
||||||
mut self,
|
|
||||||
text_color: impl Into<Color>,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.text_color.insert(text_color.into());
|
let _ = self.text_color.insert(text_color.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -660,26 +632,17 @@ impl SlideBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn stroke(
|
pub(crate) fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||||
mut self,
|
|
||||||
stroke: impl Into<Stroke>,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.stroke.insert(stroke.into());
|
let _ = self.stroke.insert(stroke.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn shadow(
|
pub(crate) fn shadow(mut self, shadow: impl Into<Shadow>) -> Self {
|
||||||
mut self,
|
|
||||||
shadow: impl Into<Shadow>,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.shadow.insert(shadow.into());
|
let _ = self.shadow.insert(shadow.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn text_alignment(
|
pub(crate) fn text_alignment(mut self, text_alignment: TextAlignment) -> Self {
|
||||||
mut self,
|
|
||||||
text_alignment: TextAlignment,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.text_alignment.insert(text_alignment);
|
let _ = self.text_alignment.insert(text_alignment);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -689,26 +652,17 @@ impl SlideBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn video_start_time(
|
pub(crate) fn video_start_time(mut self, video_start_time: f32) -> Self {
|
||||||
mut self,
|
|
||||||
video_start_time: f32,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.video_start_time.insert(video_start_time);
|
let _ = self.video_start_time.insert(video_start_time);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn video_end_time(
|
pub(crate) fn video_end_time(mut self, video_end_time: f32) -> Self {
|
||||||
mut self,
|
|
||||||
video_end_time: f32,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.video_end_time.insert(video_end_time);
|
let _ = self.video_end_time.insert(video_end_time);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn text_svg(
|
pub(crate) fn text_svg(mut self, text_svg: impl Into<TextSvg>) -> Self {
|
||||||
mut self,
|
|
||||||
text_svg: impl Into<TextSvg>,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.text_svg.insert(text_svg.into());
|
let _ = self.text_svg.insert(text_svg.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -718,10 +672,7 @@ impl SlideBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pdf_index(
|
pub(crate) fn pdf_index(mut self, pdf_index: impl Into<u32>) -> Self {
|
||||||
mut self,
|
|
||||||
pdf_index: impl Into<u32>,
|
|
||||||
) -> Self {
|
|
||||||
let _ = self.pdf_index.insert(pdf_index.into());
|
let _ = self.pdf_index.insert(pdf_index.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -779,8 +730,7 @@ mod test {
|
||||||
fn test_slide() -> Slide {
|
fn test_slide() -> Slide {
|
||||||
Slide {
|
Slide {
|
||||||
text: "This is frodo".to_string(),
|
text: "This is frodo".to_string(),
|
||||||
background: Background::try_from("~/pics/frodo.jpg")
|
background: Background::try_from("~/pics/frodo.jpg").expect(""),
|
||||||
.unwrap(),
|
|
||||||
font: Some("Quicksand".to_string().into()),
|
font: Some("Quicksand".to_string().into()),
|
||||||
font_size: 140,
|
font_size: 140,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -789,11 +739,8 @@ mod test {
|
||||||
|
|
||||||
fn test_second_slide() -> Slide {
|
fn test_second_slide() -> Slide {
|
||||||
Slide {
|
Slide {
|
||||||
text: "".to_string(),
|
text: String::new(),
|
||||||
background: Background::try_from(
|
background: Background::try_from("~/vids/test/camprules2024.mp4").expect(""),
|
||||||
"~/vids/test/camprules2024.mp4",
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
font: Some("Quicksand".to_string().into()),
|
font: Some("Quicksand".to_string().into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
@ -801,15 +748,10 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ron_deserialize() {
|
fn test_ron_deserialize() {
|
||||||
let slide = read_to_string("./test_presentation.ron")
|
let slide =
|
||||||
.expect("Problem getting file read");
|
read_to_string("./test_presentation.ron").expect("Problem getting file read");
|
||||||
match ron::from_str::<Vec<Slide>>(&slide) {
|
if let Err(e) = ron::from_str::<Vec<Slide>>(&slide) {
|
||||||
Ok(_s) => {
|
panic!("{e:?}")
|
||||||
assert!(true)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
assert!(false, "{:?}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ use miette::{IntoDiagnostic, Result};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use obws::{Client, responses::scenes::Scene};
|
use obws::Client;
|
||||||
|
use obws::responses::scenes::Scene;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,146 @@
|
||||||
|
use crate::core::songs::{Song, VerseName};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
use miette::{IntoDiagnostic, Result, miette};
|
||||||
|
use nom::branch::alt;
|
||||||
|
use nom::bytes::complete::{tag, take_till, take_till1, take_until};
|
||||||
|
use nom::character::complete::{digit0, space0};
|
||||||
|
use nom::combinator::rest;
|
||||||
|
use nom::multi::many1;
|
||||||
|
use nom::sequence::{delimited, pair};
|
||||||
|
use nom::{IResult, Parser};
|
||||||
use reqwest::header;
|
use reqwest::header;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone,
|
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||||
Debug,
|
|
||||||
Default,
|
|
||||||
PartialEq,
|
|
||||||
PartialOrd,
|
|
||||||
Ord,
|
|
||||||
Eq,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
)]
|
)]
|
||||||
pub struct OnlineSong {
|
pub struct OnlineSong {
|
||||||
pub lyrics: String,
|
pub lyrics: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub site: String,
|
pub provider: Provider,
|
||||||
pub link: String,
|
pub link: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn search_genius_links(
|
#[derive(
|
||||||
query: impl AsRef<str> + std::fmt::Display,
|
Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||||
) -> Result<Vec<OnlineSong>> {
|
)]
|
||||||
let auth_token = env!("GENIUS_TOKEN");
|
pub enum Provider {
|
||||||
|
Genius {
|
||||||
|
parsable: bool,
|
||||||
|
},
|
||||||
|
#[default]
|
||||||
|
LyricsCom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Provider {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Genius { .. } => f.write_str("Genius"),
|
||||||
|
Self::LyricsCom => f.write_str("Lyrics.com"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OnlineSong> for Song {
|
||||||
|
fn from(online_song: OnlineSong) -> Self {
|
||||||
|
let verse_map = if online_song.provider == (Provider::Genius { parsable: true }) {
|
||||||
|
parse_genius_lyrics(&online_song.lyrics.replace("\\n", "\n")).ok()
|
||||||
|
} else {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.entry(VerseName::Verse { number: 1 })
|
||||||
|
.or_insert(online_song.lyrics);
|
||||||
|
Some(map)
|
||||||
|
};
|
||||||
|
let lyrics = ron::ser::to_string(&verse_map).ok();
|
||||||
|
|
||||||
|
let verse_order: Option<Vec<String>> = verse_map
|
||||||
|
.as_ref()
|
||||||
|
.map(|map| map.keys().map(VerseName::get_name).collect());
|
||||||
|
|
||||||
|
let verses: Option<Vec<VerseName>> = verse_map
|
||||||
|
.as_ref()
|
||||||
|
.map(|map| map.keys().map(ToOwned::to_owned).collect());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
title: online_song.title,
|
||||||
|
author: Some(online_song.author),
|
||||||
|
verse_map,
|
||||||
|
lyrics,
|
||||||
|
verse_order,
|
||||||
|
verses,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||||
|
fn parse_genius_lyrics(lyrics: &str) -> Result<HashMap<VerseName, String>> {
|
||||||
|
let (input, chunks) = many1(pair(parse_verse_name, alt((take_until("["), rest))))
|
||||||
|
.parse(lyrics)
|
||||||
|
.map_err(|e| e.to_owned())
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
dbg!(input);
|
||||||
|
dbg!(&chunks);
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
for (mut name, lyric) in chunks {
|
||||||
|
while map.contains_key(&name) {
|
||||||
|
name = name.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
map.entry(name).or_insert_with(|| lyric.trim().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_verse_name(line: &str) -> IResult<&str, VerseName> {
|
||||||
|
let (input, (name, _, num, _, _)) = delimited(
|
||||||
|
(tag("["), space0),
|
||||||
|
(
|
||||||
|
take_till1(|c| c == ' ' || c == ']' || c == ':'),
|
||||||
|
space0,
|
||||||
|
digit0,
|
||||||
|
alt((tag(":"), space0)),
|
||||||
|
take_till(|c| c == ']'),
|
||||||
|
),
|
||||||
|
(space0, tag("]")),
|
||||||
|
)
|
||||||
|
.parse(line)?;
|
||||||
|
|
||||||
|
let num = num.parse::<usize>().unwrap_or(1);
|
||||||
|
dbg!(&name);
|
||||||
|
|
||||||
|
let verse_name = match name {
|
||||||
|
"Chorus" => VerseName::Chorus { number: num },
|
||||||
|
"Verse" => VerseName::Verse { number: num },
|
||||||
|
"Bridge" => VerseName::Bridge { number: num },
|
||||||
|
"Pre-Chorus" => VerseName::PreChorus { number: num },
|
||||||
|
"Post-Chorus" => VerseName::PostChorus { number: num },
|
||||||
|
"Outro" => VerseName::Outro { number: num },
|
||||||
|
"Intro" => VerseName::Intro { number: num },
|
||||||
|
"Instrumental" => VerseName::Instrumental { number: num },
|
||||||
|
_ => VerseName::Verse { number: 99 },
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((input, verse_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_genius(query: String, auth_token: String) -> Result<Vec<OnlineSong>> {
|
||||||
|
// let Some(auth_token) = option_env!("GENIUS_TOKEN") else {
|
||||||
|
// return Err(miette!("No Genius Token"));
|
||||||
|
// };
|
||||||
|
|
||||||
|
let head_value = header::HeaderValue::from_str(&auth_token).into_diagnostic()?;
|
||||||
let mut headers = header::HeaderMap::new();
|
let mut headers = header::HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(header::AUTHORIZATION, head_value);
|
||||||
header::AUTHORIZATION,
|
|
||||||
header::HeaderValue::from_static(auth_token),
|
|
||||||
);
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.default_headers(headers)
|
.default_headers(headers)
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -46,8 +155,7 @@ pub async fn search_genius_links(
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
let json: Value =
|
let json: Value = serde_json::from_str(&response).into_diagnostic()?;
|
||||||
serde_json::from_str(&response).into_diagnostic()?;
|
|
||||||
let hits = json
|
let hits = json
|
||||||
.get("response")
|
.get("response")
|
||||||
.expect("respose")
|
.expect("respose")
|
||||||
|
|
@ -55,12 +163,11 @@ pub async fn search_genius_links(
|
||||||
.expect("hits")
|
.expect("hits")
|
||||||
.as_array()
|
.as_array()
|
||||||
.expect("array");
|
.expect("array");
|
||||||
Ok(hits
|
let songs: Vec<Option<OnlineSong>> =
|
||||||
.iter()
|
cosmic::iced::futures::future::join_all(hits.iter().map(|hit| async {
|
||||||
.map(|hit| {
|
|
||||||
let result = hit.get("result").expect("result");
|
let result = hit.get("result").expect("result");
|
||||||
let title = result
|
let title = result
|
||||||
.get("full_title")
|
.get("title")
|
||||||
.expect("title")
|
.expect("title")
|
||||||
.as_str()
|
.as_str()
|
||||||
.expect("title")
|
.expect("title")
|
||||||
|
|
@ -78,20 +185,27 @@ pub async fn search_genius_links(
|
||||||
.as_str()
|
.as_str()
|
||||||
.expect("url")
|
.expect("url")
|
||||||
.to_string();
|
.to_string();
|
||||||
OnlineSong {
|
let song = OnlineSong {
|
||||||
lyrics: String::new(),
|
lyrics: String::new(),
|
||||||
title,
|
title,
|
||||||
author,
|
author,
|
||||||
site: String::from("https://genius.com"),
|
provider: Provider::Genius { parsable: false },
|
||||||
link,
|
link,
|
||||||
|
};
|
||||||
|
|
||||||
|
match get_genius_lyrics(song).await {
|
||||||
|
Ok(song) => Some(song),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Couldn't get lyrics: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
.collect())
|
.await;
|
||||||
|
Ok(songs.into_iter().flatten().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_genius_lyrics(
|
pub async fn get_genius_lyrics(mut song: OnlineSong) -> Result<OnlineSong> {
|
||||||
mut song: OnlineSong,
|
|
||||||
) -> Result<OnlineSong> {
|
|
||||||
let html = reqwest::get(&song.link)
|
let html = reqwest::get(&song.link)
|
||||||
.await
|
.await
|
||||||
.into_diagnostic()?
|
.into_diagnostic()?
|
||||||
|
|
@ -101,31 +215,64 @@ pub async fn get_genius_lyrics(
|
||||||
.await
|
.await
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
let document = scraper::Html::parse_document(&html);
|
let document = scraper::Html::parse_document(&html);
|
||||||
let Ok(lyrics_root_selector) = scraper::Selector::parse(
|
let Ok(lyrics_root_selector) =
|
||||||
r#"div[data-lyrics-container="true"]"#,
|
scraper::Selector::parse(r#"div[data-lyrics-container="true"]"#)
|
||||||
) else {
|
else {
|
||||||
return Err(miette!("error in finding lyrics_root"));
|
return Err(miette!("error in finding lyrics_root"));
|
||||||
};
|
};
|
||||||
|
|
||||||
let lyrics = document
|
let lyrics = document
|
||||||
.select(&lyrics_root_selector)
|
.select(&lyrics_root_selector)
|
||||||
.map(|root| {
|
.filter(|element| element.attr("data-exclude-from-selection").is_none())
|
||||||
|
.filter(|element| {
|
||||||
|
!element.value().classes().any(|class| {
|
||||||
|
class.contains("Contrib")
|
||||||
|
|| class.contains("LyricsHeader")
|
||||||
|
|| class.contains("StyledLink")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flat_map(|element| {
|
||||||
// dbg!(&root);
|
// dbg!(&root);
|
||||||
root.inner_html()
|
// debug!(?element);
|
||||||
|
let inner = element.inner_html().replace("<br>", "\n");
|
||||||
|
// debug!(inner);
|
||||||
|
let line_broken = scraper::Html::parse_fragment(&inner);
|
||||||
|
line_broken
|
||||||
|
.root_element()
|
||||||
|
.descendent_elements()
|
||||||
|
.filter(|element| element.attr("data-exclude-from-selection").is_none())
|
||||||
|
.filter(|element| {
|
||||||
|
let element_name = element.value().name();
|
||||||
|
element_name != "div" && element_name != "path"
|
||||||
|
})
|
||||||
|
.filter(|element| {
|
||||||
|
!element.value().classes().any(|class| {
|
||||||
|
class.contains("Contrib")
|
||||||
|
|| class.contains("LyricsHeader")
|
||||||
|
|| class.contains("StyledLink")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flat_map(|t| {
|
||||||
|
// let html = t.html();
|
||||||
|
// debug!(html);
|
||||||
|
t.text().collect::<Vec<&str>>()
|
||||||
|
})
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<String>>()
|
||||||
})
|
})
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
let lyrics = lyrics.find("[").map_or_else(
|
let lyrics = lyrics.find('[').map_or_else(
|
||||||
|| {
|
|| {
|
||||||
lyrics.find("</div></div></div>").map_or(
|
lyrics.find("</div></div></div>").map_or_else(
|
||||||
lyrics.clone(),
|
|| lyrics.clone(),
|
||||||
|position| {
|
|position| lyrics.split_at(position + 18).1.to_string(),
|
||||||
lyrics.split_at(position + 18).1.to_string()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|position| lyrics.split_at(position).1.to_string(),
|
|position| lyrics.split_at(position).1.to_string(),
|
||||||
);
|
);
|
||||||
let lyrics = lyrics.replace("<br>", "\n");
|
song.provider = Provider::Genius {
|
||||||
|
parsable: lyrics.contains('['),
|
||||||
|
};
|
||||||
song.lyrics = lyrics;
|
song.lyrics = lyrics;
|
||||||
Ok(song)
|
Ok(song)
|
||||||
}
|
}
|
||||||
|
|
@ -133,20 +280,17 @@ pub async fn get_genius_lyrics(
|
||||||
pub async fn search_lyrics_com_links(
|
pub async fn search_lyrics_com_links(
|
||||||
query: impl AsRef<str> + std::fmt::Display,
|
query: impl AsRef<str> + std::fmt::Display,
|
||||||
) -> Result<Vec<String>> {
|
) -> Result<Vec<String>> {
|
||||||
let html =
|
let html = reqwest::get(format!("http://www.lyrics.com/lyrics/{query}"))
|
||||||
reqwest::get(format!("http://www.lyrics.com/lyrics/{query}"))
|
.await
|
||||||
.await
|
.into_diagnostic()?
|
||||||
.into_diagnostic()?
|
.error_for_status()
|
||||||
.error_for_status()
|
.into_diagnostic()?
|
||||||
.into_diagnostic()?
|
.text()
|
||||||
.text()
|
.await
|
||||||
.await
|
.into_diagnostic()?;
|
||||||
.into_diagnostic()?;
|
|
||||||
|
|
||||||
let document = scraper::Html::parse_document(&html);
|
let document = scraper::Html::parse_document(&html);
|
||||||
let Ok(best_matches_selector) =
|
let Ok(best_matches_selector) = scraper::Selector::parse(".best-matches") else {
|
||||||
scraper::Selector::parse(".best-matches")
|
|
||||||
else {
|
|
||||||
return Err(miette!("error in finding matches"));
|
return Err(miette!("error in finding matches"));
|
||||||
};
|
};
|
||||||
let Ok(lyric_selector) = scraper::Selector::parse("a") else {
|
let Ok(lyric_selector) = scraper::Selector::parse("a") else {
|
||||||
|
|
@ -156,9 +300,7 @@ pub async fn search_lyrics_com_links(
|
||||||
Ok(document
|
Ok(document
|
||||||
.select(&best_matches_selector)
|
.select(&best_matches_selector)
|
||||||
.flat_map(|best_section| best_section.select(&lyric_selector))
|
.flat_map(|best_section| best_section.select(&lyric_selector))
|
||||||
.map(|a| {
|
.map(|a| a.value().attr("href").unwrap_or("").trim().to_string())
|
||||||
a.value().attr("href").unwrap_or("").trim().to_string()
|
|
||||||
})
|
|
||||||
.filter(|a| a.contains("/lyric/"))
|
.filter(|a| a.contains("/lyric/"))
|
||||||
.dedup()
|
.dedup()
|
||||||
.map(|link| {
|
.map(|link| {
|
||||||
|
|
@ -198,9 +340,7 @@ pub async fn lyrics_com_link_to_song(
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
|
|
||||||
let document = scraper::Html::parse_document(&html);
|
let document = scraper::Html::parse_document(&html);
|
||||||
let Ok(lyric_selector) =
|
let Ok(lyric_selector) = scraper::Selector::parse(".lyric-body") else {
|
||||||
scraper::Selector::parse(".lyric-body")
|
|
||||||
else {
|
|
||||||
return Err(miette!("error in finding lyric-body",));
|
return Err(miette!("error in finding lyric-body",));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -215,7 +355,7 @@ pub async fn lyrics_com_link_to_song(
|
||||||
lyrics,
|
lyrics,
|
||||||
title: title.clone(),
|
title: title.clone(),
|
||||||
author: author.clone(),
|
author: author.clone(),
|
||||||
site: "https://www.lyrics.com".into(),
|
provider: Provider::LyricsCom,
|
||||||
link,
|
link,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -236,35 +376,57 @@ mod test {
|
||||||
async fn genius() -> Result<(), String> {
|
async fn genius() -> Result<(), String> {
|
||||||
let song = OnlineSong {
|
let song = OnlineSong {
|
||||||
lyrics: String::new(),
|
lyrics: String::new(),
|
||||||
title: "Death Was Arrested by North Point Worship (Ft. Seth Condrey)".to_string(),
|
title: "Death Was Arrested".to_string(),
|
||||||
author: "North Point Worship (Ft. Seth Condrey)".to_string(),
|
author: "North Point Worship (Ft. Seth Condrey)".to_string(),
|
||||||
site: "https://genius.com".to_string(),
|
provider: Provider::Genius { parsable: false },
|
||||||
link: "https://genius.com/North-point-worship-death-was-arrested-lyrics".to_string(),
|
link: "https://genius.com/North-point-worship-death-was-arrested-lyrics"
|
||||||
|
.to_string(),
|
||||||
};
|
};
|
||||||
let hits = search_genius_links("Death was arrested")
|
let hits = search_genius(
|
||||||
.await
|
"Death was arrested".to_string(),
|
||||||
.map_err(|e| e.to_string())?;
|
env!("GENIUS_TOKEN").to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
hits.iter().find(|hit| **hit == song).is_some(),
|
hits[0].title == song.title,
|
||||||
"There was no song that matched on Genius"
|
"There was no song that matched on Genius"
|
||||||
);
|
);
|
||||||
|
|
||||||
let titles: Vec<String> =
|
let titles: Vec<String> = hits.iter().map(|song| song.title.clone()).collect();
|
||||||
hits.iter().map(|song| song.title.clone()).collect();
|
|
||||||
dbg!(titles);
|
dbg!(titles);
|
||||||
for hit in hits {
|
for hit in hits {
|
||||||
let new_song = get_genius_lyrics(hit)
|
let new_song = get_genius_lyrics(hit).await.map_err(|e| e.to_string())?;
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
dbg!(&new_song);
|
dbg!(&new_song);
|
||||||
if !new_song.lyrics.starts_with("[Verse 1]") {
|
dbg!(&new_song.provider);
|
||||||
assert!(new_song.lyrics.len() > 10);
|
if new_song.lyrics.starts_with("[Verse 1]") {
|
||||||
} else {
|
|
||||||
assert!(new_song.lyrics.contains("[Verse 2]"));
|
assert!(new_song.lyrics.contains("[Verse 2]"));
|
||||||
if !new_song.lyrics.contains("[Chorus]") {
|
if !new_song.lyrics.contains("[Chorus]") {
|
||||||
assert!(new_song.lyrics.contains("[Chorus 1]"))
|
assert!(new_song.lyrics.contains("[Chorus 1]"));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
assert!(new_song.lyrics.len() > 10);
|
||||||
|
}
|
||||||
|
let mapped_song = Song::from(new_song);
|
||||||
|
dbg!(&mapped_song);
|
||||||
|
if let Some(map) = mapped_song.verse_map.as_ref() {
|
||||||
|
assert!(!map.is_empty());
|
||||||
|
// Need to leave commented until I work on more robust tests.
|
||||||
|
assert!(
|
||||||
|
map.keys().contains(&VerseName::Verse { number: 1 }) // && map.keys().contains(&VerseName::Verse {
|
||||||
|
// number: 2
|
||||||
|
// })
|
||||||
|
// && map.keys().contains(&VerseName::Chorus {
|
||||||
|
// number: 1
|
||||||
|
// })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert!(
|
||||||
|
!mapped_song
|
||||||
|
.lyrics
|
||||||
|
.is_some_and(|lyrics| lyrics.contains('['))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,7 +439,7 @@ mod test {
|
||||||
lyrics: "Alone in my sorrow and dead in my sin\nLost without hope with no place to begin\nYour love Made a way to let mercy come in\nWhen death was arrested and my life began\n\nAsh was redeemed only beauty remains\nMy orphan heart was given a name\nMy mourning grew quiet my feet rose to dance\nWhen death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nReleased from my chains I'm a prisoner no more\nMy shame was a ransom He faithfully bore\nHe cancelled my debt and He called me His friend\nWhen death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nOur savior displayed on a criminal's cross\nDarkness rejoiced as though heaven had lost\nBut then Jesus arose with our freedom in hand\nThat's when death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nOh, we're free, free\nForever we're free\nCome join the song\nOf all the redeemed\nYes, we're free free\nForever amen\nWhen death was arrested and my life began\n\nOh, we're free, free\nForever we're free\nCome join the song\nOf all the redeemed\nYes, we're free free\nForever amen\nWhen death was arrested and my life began\n\nWhen death was arrested and my life began\nWhen death was arrested and my life began".to_string(),
|
lyrics: "Alone in my sorrow and dead in my sin\nLost without hope with no place to begin\nYour love Made a way to let mercy come in\nWhen death was arrested and my life began\n\nAsh was redeemed only beauty remains\nMy orphan heart was given a name\nMy mourning grew quiet my feet rose to dance\nWhen death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nReleased from my chains I'm a prisoner no more\nMy shame was a ransom He faithfully bore\nHe cancelled my debt and He called me His friend\nWhen death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nOur savior displayed on a criminal's cross\nDarkness rejoiced as though heaven had lost\nBut then Jesus arose with our freedom in hand\nThat's when death was arrested and my life began\n\nOh, Your grace so free\nWashes over me\nYou have made me new\nNow life begins with You\nIt's your endless love\nPouring down on us\nYou have made us new\nNow life begins with You\n\nOh, we're free, free\nForever we're free\nCome join the song\nOf all the redeemed\nYes, we're free free\nForever amen\nWhen death was arrested and my life began\n\nOh, we're free, free\nForever we're free\nCome join the song\nOf all the redeemed\nYes, we're free free\nForever amen\nWhen death was arrested and my life began\n\nWhen death was arrested and my life began\nWhen death was arrested and my life began".to_string(),
|
||||||
title: "Death Was Arrested".to_string(),
|
title: "Death Was Arrested".to_string(),
|
||||||
author: "North Point InsideOut".to_string(),
|
author: "North Point InsideOut".to_string(),
|
||||||
site: "https://www.lyrics.com".to_string(),
|
provider: Provider::LyricsCom,
|
||||||
link: "https://www.lyrics.com/lyric/35090938/North+Point+InsideOut/Death+Was+Arrested".to_string(),
|
link: "https://www.lyrics.com/lyric/35090938/North+Point+InsideOut/Death+Was+Arrested".to_string(),
|
||||||
};
|
};
|
||||||
let links = search_lyrics_com_links("Death was arrested")
|
let links = search_lyrics_com_links("Death was arrested")
|
||||||
|
|
@ -286,47 +448,131 @@ mod test {
|
||||||
let songs = lyrics_com_link_to_song(links)
|
let songs = lyrics_com_link_to_song(links)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("{e}"))?;
|
.map_err(|e| format!("{e}"))?;
|
||||||
if let Some(first) = songs.iter().find_or_first(|song| {
|
if let Some(first) = songs
|
||||||
song.author == "North Point InsideOut"
|
.iter()
|
||||||
}) {
|
.find_or_first(|song| song.author == "North Point InsideOut")
|
||||||
|
{
|
||||||
assert_eq!(&song, first);
|
assert_eq!(&song, first);
|
||||||
online_song_to_song(song)?
|
// online_song_to_song(song)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
fn online_song_to_song(song: OnlineSong) -> Result<(), String> {
|
fn online_song_to_song(song: OnlineSong) -> Result<(), String> {
|
||||||
let song = Song::from(song);
|
let song = Song::from(song);
|
||||||
if let Some(verse_map) = song.verse_map.as_ref() {
|
if let Some(verse_map) = song.verse_map.as_ref() {
|
||||||
if verse_map.len() < 2 {
|
if verse_map.is_empty() {
|
||||||
return Err(format!(
|
return Err(format!("VerseMap wasn't built right likely: {song:?}",));
|
||||||
"VerseMap wasn't built right likely: {:?}",
|
|
||||||
song
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(String::from(
|
return Err(String::from("There is no VerseMap in this song"));
|
||||||
"There is no VerseMap in this song",
|
}
|
||||||
));
|
|
||||||
};
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
// #[tokio::test]
|
||||||
async fn online_search() {
|
// async fn online_search() {
|
||||||
let search =
|
// let search =
|
||||||
search_lyrics_com_links("Death was arrested").await;
|
// search_lyrics_com_links("Death was arrested").await;
|
||||||
match search {
|
// match search {
|
||||||
Ok(songs) => {
|
// Ok(songs) => {
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
songs,
|
// songs,
|
||||||
vec![
|
// vec![
|
||||||
"33755723/Various+Artists/Death+Was+Arrested",
|
// "33755723/Various+Artists/Death+Was+Arrested",
|
||||||
"35090938/North+Point+InsideOut/Death+Was+Arrested"
|
// "35090938/North+Point+InsideOut/Death+Was+Arrested"
|
||||||
]
|
// ]
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
Err(e) => assert!(false, "{}", e),
|
// Err(e) => panic!("{e}"),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::redundant_closure_for_method_calls)]
|
||||||
|
fn test_parse_verse_name() -> Result<()> {
|
||||||
|
let names = [
|
||||||
|
"[ Chorus ]",
|
||||||
|
"[Verse 1]",
|
||||||
|
"[Pre-Chorus]",
|
||||||
|
"[ Post-Chorus ]",
|
||||||
|
"[ Post-Chorus 3]",
|
||||||
|
"[Verse 2]",
|
||||||
|
"[Verse 3]",
|
||||||
|
"[Verse 4:]",
|
||||||
|
"[Verse 5: Coffee]",
|
||||||
|
"[Chorus 1]",
|
||||||
|
"[ Chorus 2 ]",
|
||||||
|
];
|
||||||
|
for name in names {
|
||||||
|
let (_input, parsed) = parse_verse_name
|
||||||
|
.parse(name)
|
||||||
|
.map_err(|e| e.to_owned())
|
||||||
|
.into_diagnostic()?;
|
||||||
|
dbg!(parsed);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_song() -> Result<()> {
|
||||||
|
let song = r#"[Verse 1]
|
||||||
|
Glory, glory
|
||||||
|
I've been singing
|
||||||
|
Since I laid my burden down
|
||||||
|
Glory, glory
|
||||||
|
I've been singing
|
||||||
|
Since I laid my burden down
|
||||||
|
|
||||||
|
[Chorus]
|
||||||
|
I'm singing, "Hallelujah"
|
||||||
|
God is able, hallelujah
|
||||||
|
God is faithful, hallelujah
|
||||||
|
Lord, I'm gonna sing
|
||||||
|
|
||||||
|
[Verse 2]
|
||||||
|
I feel better
|
||||||
|
So much better
|
||||||
|
Since I laid my burden down
|
||||||
|
Yeah, I feel better
|
||||||
|
So much better
|
||||||
|
Since I laid, O Lord, I laid my burden down
|
||||||
|
|
||||||
|
[Chorus]
|
||||||
|
I'm singing, "Hallelujah"
|
||||||
|
God is able, hallelujah
|
||||||
|
God is faithful, hallelujah
|
||||||
|
Lord, I'm gonna sing
|
||||||
|
I'm singing, "Hallelujah"
|
||||||
|
God is able, hallelujah
|
||||||
|
God is faithful, hallelujah
|
||||||
|
Lord, I'm gonna sing
|
||||||
|
|
||||||
|
[Bridge]
|
||||||
|
As long as I'm alive there's gonna be praising
|
||||||
|
As long as I'm alive there's gonna be shouting
|
||||||
|
One thing that I know, oh, deep down in my soul
|
||||||
|
As long as I'm alive, I'm gonna sing
|
||||||
|
|
||||||
|
[Chorus]
|
||||||
|
I'm singing, "Hallelujah" (Hallelujah)
|
||||||
|
God is able, hallelujah (Hallelujah)
|
||||||
|
God is faithful, hallelujah
|
||||||
|
Lord, I'm gonna sing (Come on now, sing it)
|
||||||
|
Oh I'm singing, "Hallelujah" (Hallelujah)
|
||||||
|
God is able, hallelujah (Hallelujah)
|
||||||
|
God is faithful, hallelujah (God is so good)
|
||||||
|
Lord, I'm gonna sing (Sing it, Dave)
|
||||||
|
|
||||||
|
[Outro]
|
||||||
|
I'm gonna sing
|
||||||
|
Aw man, that was good"#;
|
||||||
|
let new_song = r"[Verse 1]\nAlone in my sorrow and dead in my sin\nLost without hope with no place to begin\nYour love made a way to let mercy come in\nWhen death was arrested and my life began\nAsh was redeemed, only beauty remains\nMy orphan heart was given a name\nMy mourning grew quiet, my feet rose to dance\nWhen death was arrested and my life began\n\n[Chorus]\nOh, Your grace so free, washes over me\nYou have made me new, now life begins with You\nIt's Your endless love, pouring down on us\nYou have made us new, now life begins with You\n\n[Verse 2]\nReleased from my chains, I'm a prisoner no more\nMy shame was a ransom He faithfully bore\nHe cancelled my debt and He called me His friend\nWhen death was arrested and my life began\n\n[Chorus]\nOh, Your grace so free, washes over me\nYou have made me new, now life begins with You\nIt's Your endless love, pouring down on us\nYou have made us new, now life begins with You\n[Verse 3]\nOur Savior displayed on a criminal's cross\nDarkness rejoiced as though heaven had lost\nBut then Jesus arose with our freedom in hand\nThat's when death was arrested and my life began\n\n[Chorus]\nOh, Your grace so free, washes over me\nYou have made me new, now life begins with You\nIt's Your endless love, pouring down on us\nYou have made us new, now life begins with You\n\n[Outro]\nOh, we're free, free, forever we're free\nCome join the song of all the redeemed\nYes, we're free, free, forever amen\nWhen death was arrested and my life began\nOh, we're free, free, forever we're free\nCome join the song of all the redeemed\nYes, we're free, free, forever amen\nWhen death was arrested and my life began\nWhen death was arrested and my life began\nWhen death was arrested and my life began".replace("\\n", "\n");
|
||||||
|
let map = parse_genius_lyrics(song)?;
|
||||||
|
let new_map = parse_genius_lyrics(&new_song)?;
|
||||||
|
dbg!(map);
|
||||||
|
dbg!(new_map);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
use dirs;
|
use dirs;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs;
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::str;
|
use std::{fs, str};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
pub fn bg_from_video(
|
pub fn bg_from_video(video: &Path, screenshot: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
video: &Path,
|
|
||||||
screenshot: &Path,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
if screenshot.exists() {
|
if screenshot.exists() {
|
||||||
debug!("Screenshot already exists");
|
debug!("Screenshot already exists");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -40,10 +36,8 @@ pub fn bg_from_video(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let hours: i32 = hours.parse().unwrap_or_default();
|
let hours: i32 = hours.parse().unwrap_or_default();
|
||||||
let mut minutes: i32 =
|
let mut minutes: i32 = minutes.parse().unwrap_or_default();
|
||||||
minutes.parse().unwrap_or_default();
|
let mut seconds: i32 = seconds.parse().unwrap_or_default();
|
||||||
let mut seconds: i32 =
|
|
||||||
seconds.parse().unwrap_or_default();
|
|
||||||
minutes += hours * 60;
|
minutes += hours * 60;
|
||||||
seconds += minutes * 60;
|
seconds += minutes * 60;
|
||||||
at_second = seconds / 5;
|
at_second = seconds / 5;
|
||||||
|
|
@ -71,18 +65,15 @@ pub fn bg_from_video(
|
||||||
pub fn bg_path_from_video(video: &Path) -> PathBuf {
|
pub fn bg_path_from_video(video: &Path) -> PathBuf {
|
||||||
let video = PathBuf::from(video);
|
let video = PathBuf::from(video);
|
||||||
debug!(?video);
|
debug!(?video);
|
||||||
let mut data_dir =
|
let mut data_dir = dirs::cache_dir().expect("Can't find cache dir");
|
||||||
dirs::cache_dir().expect("Can't find cache dir");
|
|
||||||
data_dir.push("lumina");
|
data_dir.push("lumina");
|
||||||
data_dir.push("thumbnails");
|
data_dir.push("thumbnails");
|
||||||
let _ = fs::create_dir_all(&data_dir);
|
let _ = fs::create_dir_all(&data_dir);
|
||||||
if !data_dir.exists() {
|
if !data_dir.exists() {
|
||||||
fs::create_dir(&data_dir)
|
fs::create_dir(&data_dir).expect("Could not create thumbnails dir");
|
||||||
.expect("Could not create thumbnails dir");
|
|
||||||
}
|
}
|
||||||
let mut screenshot = data_dir.clone();
|
let mut screenshot = data_dir.clone();
|
||||||
screenshot
|
screenshot.push(video.file_name().expect("Should have file name"));
|
||||||
.push(video.file_name().expect("Should have file name"));
|
|
||||||
screenshot.set_extension("png");
|
screenshot.set_extension("png");
|
||||||
screenshot
|
screenshot
|
||||||
}
|
}
|
||||||
|
|
@ -97,11 +88,9 @@ mod test {
|
||||||
let screenshot = bg_path_from_video(video);
|
let screenshot = bg_path_from_video(video);
|
||||||
match bg_from_video(video, &screenshot) {
|
match bg_from_video(video, &screenshot) {
|
||||||
Ok(_o) => assert!(screenshot.exists()),
|
Ok(_o) => assert!(screenshot.exists()),
|
||||||
Err(e) => debug_assert!(
|
Err(e) => {
|
||||||
false,
|
debug_assert!(false, "There was an error in the runtime future. {e}",);
|
||||||
"There was an error in the runtime future. {:?}",
|
}
|
||||||
e
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,23 @@
|
||||||
|
use crate::core::model::{Sort, SortDirection};
|
||||||
use crate::{Background, SlideBuilder, TextAlignment};
|
use crate::{Background, SlideBuilder, TextAlignment};
|
||||||
|
|
||||||
use super::{
|
use super::content::Content;
|
||||||
content::Content,
|
use super::kinds::ServiceItemKind;
|
||||||
kinds::ServiceItemKind,
|
use super::model::{LibraryKind, Model};
|
||||||
model::{LibraryKind, Model},
|
use super::service_items::ServiceTrait;
|
||||||
service_items::ServiceTrait,
|
use super::slide::Slide;
|
||||||
slide::Slide,
|
|
||||||
};
|
|
||||||
use crisp::types::{Keyword, Symbol, Value};
|
use crisp::types::{Keyword, Symbol, Value};
|
||||||
use miette::{IntoDiagnostic, Result};
|
use itertools::Itertools;
|
||||||
|
use miette::{IntoDiagnostic, Result, miette};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::types::chrono::{DateTime, Local};
|
||||||
Sqlite, SqliteConnection, SqlitePool, pool::PoolConnection,
|
use sqlx::{AssertSqlSafe, Decode, SqliteConnection, SqlitePool, query, query_as};
|
||||||
query, query_as,
|
use std::mem::replace;
|
||||||
};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tracing::{debug, error};
|
use std::sync::Arc;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Decode)]
|
||||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Video {
|
pub struct Video {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
@ -27,6 +25,10 @@ pub struct Video {
|
||||||
pub start_time: Option<f32>,
|
pub start_time: Option<f32>,
|
||||||
pub end_time: Option<f32>,
|
pub end_time: Option<f32>,
|
||||||
pub looping: bool,
|
pub looping: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub accessed_at: DateTime<Local>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Video> for Value {
|
impl From<&Video> for Value {
|
||||||
|
|
@ -97,30 +99,21 @@ impl From<&Value> for Video {
|
||||||
Value::List(list) => {
|
Value::List(list) => {
|
||||||
let path = list
|
let path = list
|
||||||
.iter()
|
.iter()
|
||||||
.position(|v| {
|
.position(|v| v == &Value::Keyword(Keyword::from("source")))
|
||||||
v == &Value::Keyword(Keyword::from("source"))
|
|
||||||
})
|
|
||||||
.and_then(|path_pos| {
|
.and_then(|path_pos| {
|
||||||
let pos = path_pos + 1;
|
let pos = path_pos + 1;
|
||||||
list.get(pos)
|
list.get(pos).map(|p| PathBuf::from(String::from(p)))
|
||||||
.map(|p| PathBuf::from(String::from(p)))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let title = path.clone().map(|p| {
|
let title = path.clone().map(|p| {
|
||||||
let path =
|
let path = p.to_str().unwrap_or_default().to_string();
|
||||||
p.to_str().unwrap_or_default().to_string();
|
let title = path.rsplit_once('/').unwrap_or_default().1;
|
||||||
let title =
|
|
||||||
path.rsplit_once('/').unwrap_or_default().1;
|
|
||||||
title.to_string()
|
title.to_string()
|
||||||
});
|
});
|
||||||
|
|
||||||
let start_time = list
|
let start_time = list
|
||||||
.iter()
|
.iter()
|
||||||
.position(|v| {
|
.position(|v| v == &Value::Keyword(Keyword::from("start-time")))
|
||||||
v == &Value::Keyword(Keyword::from(
|
|
||||||
"start-time",
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.and_then(|start_pos| {
|
.and_then(|start_pos| {
|
||||||
let pos = start_pos + 1;
|
let pos = start_pos + 1;
|
||||||
list.get(pos).map(|p| i32::from(p) as f32)
|
list.get(pos).map(|p| i32::from(p) as f32)
|
||||||
|
|
@ -128,11 +121,7 @@ impl From<&Value> for Video {
|
||||||
|
|
||||||
let end_time = list
|
let end_time = list
|
||||||
.iter()
|
.iter()
|
||||||
.position(|v| {
|
.position(|v| v == &Value::Keyword(Keyword::from("end-time")))
|
||||||
v == &Value::Keyword(Keyword::from(
|
|
||||||
"end-time",
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.and_then(|end_pos| {
|
.and_then(|end_pos| {
|
||||||
let pos = end_pos + 1;
|
let pos = end_pos + 1;
|
||||||
list.get(pos).map(|p| i32::from(p) as f32)
|
list.get(pos).map(|p| i32::from(p) as f32)
|
||||||
|
|
@ -140,14 +129,10 @@ impl From<&Value> for Video {
|
||||||
|
|
||||||
let looping = list
|
let looping = list
|
||||||
.iter()
|
.iter()
|
||||||
.position(|v| {
|
.position(|v| v == &Value::Keyword(Keyword::from("loop")))
|
||||||
v == &Value::Keyword(Keyword::from("loop"))
|
|
||||||
})
|
|
||||||
.is_some_and(|loop_pos| {
|
.is_some_and(|loop_pos| {
|
||||||
let pos = loop_pos + 1;
|
let pos = loop_pos + 1;
|
||||||
list.get(pos).is_some_and(|l| {
|
list.get(pos).is_some_and(|l| String::from(l) == *"true")
|
||||||
String::from(l) == *"true"
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -175,10 +160,7 @@ impl ServiceTrait for Video {
|
||||||
|
|
||||||
fn to_slides(&self) -> Result<Vec<Slide>> {
|
fn to_slides(&self) -> Result<Vec<Slide>> {
|
||||||
let slide = SlideBuilder::new()
|
let slide = SlideBuilder::new()
|
||||||
.background(
|
.background(Background::try_from(self.path.clone()).into_diagnostic()?)
|
||||||
Background::try_from(self.path.clone())
|
|
||||||
.into_diagnostic()?,
|
|
||||||
)
|
|
||||||
.text("")
|
.text("")
|
||||||
.audio("")
|
.audio("")
|
||||||
.font("")
|
.font("")
|
||||||
|
|
@ -198,19 +180,18 @@ impl ServiceTrait for Video {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model<Video> {
|
impl Model<Video> {
|
||||||
pub async fn new_video_model(db: &mut SqlitePool) -> Self {
|
pub async fn new_video_model(db: Arc<SqlitePool>) -> Self {
|
||||||
let mut model = Self {
|
let mut model = Self {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Video,
|
kind: LibraryKind::Video,
|
||||||
|
sorting_method: Sort::AccessTime(SortDirection::Descending),
|
||||||
};
|
};
|
||||||
let mut db = db.acquire().await.expect("probs");
|
model.load_from_db(db).await;
|
||||||
|
|
||||||
model.load_from_db(&mut db).await;
|
|
||||||
model
|
model
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_from_db(&mut self, db: &mut SqliteConnection) {
|
pub async fn load_from_db(&mut self, db: Arc<SqlitePool>) {
|
||||||
let result = query_as!(Video, r#"SELECT title as "title!", file_path as "path!", start_time as "start_time!: f32", end_time as "end_time!: f32", loop as "looping!", id as "id: i32" from videos"#).fetch_all(db).await;
|
let result = query_as!(Video, r#"SELECT title as "title!", file_path as "path!", start_time as "start_time!: f32", end_time as "end_time!: f32", loop as "looping!", id as "id: i32", accessed_at as "accessed_at!: DateTime<Local>", created_at as "created_at!: DateTime<Local>" from videos"#).fetch_all(&*db).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
for video in v {
|
for video in v {
|
||||||
|
|
@ -218,61 +199,129 @@ impl Model<Video> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!("There was an error in converting videos: {e}");
|
||||||
"There was an error in converting videos: {e}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sort(&mut self) {
|
||||||
|
match self.sorting_method {
|
||||||
|
Sort::AccessTime(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.accessed_at.cmp(&a.accessed_at))
|
||||||
|
}
|
||||||
|
Sort::AccessTime(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.accessed_at.cmp(&b.accessed_at))
|
||||||
|
}
|
||||||
|
Sort::Title(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.title.cmp(&a.title))
|
||||||
|
}
|
||||||
|
Sort::Title(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.title.cmp(&b.title))
|
||||||
|
}
|
||||||
|
Sort::CreatedTime(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.created_at.cmp(&a.created_at))
|
||||||
|
}
|
||||||
|
Sort::CreatedTime(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.created_at.cmp(&b.created_at))
|
||||||
|
}
|
||||||
|
Sort::Secondary(SortDirection::Descending) => {
|
||||||
|
self.items.sort_by(|a, b| b.path.cmp(&a.path))
|
||||||
|
}
|
||||||
|
Sort::Secondary(SortDirection::Ascending) => {
|
||||||
|
self.items.sort_by(|a, b| a.path.cmp(&b.path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sort(mut self, method: Sort) -> Self {
|
||||||
|
self.sorting_method = method;
|
||||||
|
self.sort();
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_from_db(
|
pub async fn remove_videos(
|
||||||
db: PoolConnection<Sqlite>,
|
db: Arc<SqlitePool>,
|
||||||
id: i32,
|
videos: Vec<Video>,
|
||||||
) -> Result<()> {
|
ids: Vec<i32>,
|
||||||
query!("DELETE FROM videos WHERE id = $1", id)
|
) -> Result<Vec<Video>> {
|
||||||
.execute(&mut db.detach())
|
let videos = videos
|
||||||
|
.into_iter()
|
||||||
|
.filter(|current_video| !ids.contains(¤t_video.id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let delete = format!(
|
||||||
|
"DELETE FROM videos WHERE id IN ({:})",
|
||||||
|
ids.iter().map(ToString::to_string).join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
query(AssertSqlSafe(delete))
|
||||||
|
.execute(&*db)
|
||||||
.await
|
.await
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
.map(|_| ())
|
.map(|_| videos)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_video_to_db(
|
pub async fn remove_video(
|
||||||
|
db: Arc<SqlitePool>,
|
||||||
|
mut videos: Vec<Video>,
|
||||||
|
id: i32,
|
||||||
|
) -> Result<Vec<Video>> {
|
||||||
|
query!("DELETE FROM videos WHERE id = $1", id)
|
||||||
|
.execute(&*db)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()
|
||||||
|
.map(|_| ())?;
|
||||||
|
|
||||||
|
let index = videos
|
||||||
|
.iter()
|
||||||
|
.position(|current_video| current_video.id == id)
|
||||||
|
.ok_or_else(|| miette!("Could not find video in model"))?;
|
||||||
|
videos.remove(index);
|
||||||
|
Ok(videos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_video(
|
||||||
|
new_videos: Vec<Video>,
|
||||||
|
mut current_videos: Vec<Video>,
|
||||||
|
db: Arc<SqlitePool>,
|
||||||
|
) -> Result<Vec<Video>> {
|
||||||
|
for video in new_videos {
|
||||||
|
let path = video
|
||||||
|
.path
|
||||||
|
.to_str()
|
||||||
|
.map(std::string::ToString::to_string)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
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(&*db)
|
||||||
|
.await
|
||||||
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
current_videos.push(video);
|
||||||
|
}
|
||||||
|
Ok(current_videos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_video(
|
||||||
video: Video,
|
video: Video,
|
||||||
db: PoolConnection<Sqlite>,
|
mut videos: Vec<Video>,
|
||||||
) -> Result<()> {
|
db: Arc<SqlitePool>,
|
||||||
|
) -> Result<Vec<Video>> {
|
||||||
let path = video
|
let path = video
|
||||||
.path
|
.path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(std::string::ToString::to_string)
|
.map(std::string::ToString::to_string)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mut db = db.detach();
|
|
||||||
query!(
|
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(
|
|
||||||
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();
|
|
||||||
debug!(?video, "should be been updated");
|
|
||||||
let result = query!(
|
|
||||||
r#"UPDATE videos SET title = $2, file_path = $3, start_time = $4, end_time = $5, loop = $6 WHERE id = $1"#,
|
r#"UPDATE videos SET title = $2, file_path = $3, start_time = $4, end_time = $5, loop = $6 WHERE id = $1"#,
|
||||||
video.id,
|
video.id,
|
||||||
video.title,
|
video.title,
|
||||||
|
|
@ -281,26 +330,25 @@ pub async fn update_video_in_db(
|
||||||
video.end_time,
|
video.end_time,
|
||||||
video.looping,
|
video.looping,
|
||||||
)
|
)
|
||||||
.execute(&mut db)
|
.execute(&*db)
|
||||||
.await.into_diagnostic();
|
.await.into_diagnostic()?;
|
||||||
|
|
||||||
match result {
|
let current_video = videos
|
||||||
Ok(_) => {
|
.iter()
|
||||||
debug!("should have been updated");
|
.position(|current_video| current_video.id == video.id)
|
||||||
Ok(())
|
.ok_or_else(|| miette!("Could not find video in model"))
|
||||||
}
|
.map(|index| {
|
||||||
Err(e) => {
|
videos
|
||||||
error! {?e};
|
.get_mut(index)
|
||||||
Err(e)
|
.expect("We should have this video already")
|
||||||
}
|
})?;
|
||||||
}
|
|
||||||
|
let _ = replace(current_video, video);
|
||||||
|
Ok(videos)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_video_from_db(
|
pub async fn get_from_db(database_id: i32, db: &mut SqliteConnection) -> Result<Video> {
|
||||||
database_id: i32,
|
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", accessed_at as "accessed_at!: DateTime<Local>", created_at as "created_at!: DateTime<Local>" from videos where id = ?"#, database_id).fetch_one(db).await.into_diagnostic()
|
||||||
db: &mut SqliteConnection,
|
|
||||||
) -> Result<Video> {
|
|
||||||
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 where id = ?"#, database_id).fetch_one(db).await.into_diagnostic()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -312,7 +360,7 @@ mod test {
|
||||||
Video {
|
Video {
|
||||||
title,
|
title,
|
||||||
path: PathBuf::from(
|
path: PathBuf::from(
|
||||||
"/home/chris/docs/notes/lessons/christ-our-hope.mp4",
|
"/home/chris/nc/tfc/Documents/lessons/videos/christ-nutshell.mp4",
|
||||||
),
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
@ -323,14 +371,15 @@ mod test {
|
||||||
let mut video_model: Model<Video> = Model {
|
let mut video_model: Model<Video> = Model {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Video,
|
kind: LibraryKind::Video,
|
||||||
|
sorting_method: Sort::AccessTime(SortDirection::Descending),
|
||||||
};
|
};
|
||||||
let mut db = add_db().await.unwrap().acquire().await.unwrap();
|
let db = Arc::new(add_db().await.expect(""));
|
||||||
video_model.load_from_db(&mut db).await;
|
video_model.load_from_db(db).await;
|
||||||
if let Some(video) = video_model.find(|v| v.id == 2) {
|
if let Some(video) = video_model.find(|v| v.id == 2) {
|
||||||
let test_video = test_video("christ-our-hope.mp4".into());
|
let test_video = test_video("christ-our-hope.mp4".into());
|
||||||
assert_eq!(test_video.title, video.title);
|
assert_eq!(test_video.title, video.title);
|
||||||
} else {
|
} else {
|
||||||
assert!(false);
|
panic!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,25 +389,18 @@ mod test {
|
||||||
let mut video_model: Model<Video> = Model {
|
let mut video_model: Model<Video> = Model {
|
||||||
items: vec![],
|
items: vec![],
|
||||||
kind: LibraryKind::Video,
|
kind: LibraryKind::Video,
|
||||||
|
sorting_method: Sort::AccessTime(SortDirection::Descending),
|
||||||
};
|
};
|
||||||
let result = video_model.add_item(video.clone());
|
let result = video_model.add_item(video.clone());
|
||||||
let new_video = test_video("A newer video".into());
|
let new_video = test_video("A newer video".into());
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(()) => {
|
||||||
assert_eq!(
|
assert_eq!(&video, video_model.find(|v| v.id == 0).expect(""));
|
||||||
&video,
|
assert_ne!(&new_video, video_model.find(|v| v.id == 0).expect(""));
|
||||||
video_model.find(|v| v.id == 0).unwrap()
|
}
|
||||||
);
|
Err(e) => {
|
||||||
assert_ne!(
|
panic!("There was an error adding the video: {e}",)
|
||||||
&new_video,
|
|
||||||
video_model.find(|v| v.id == 0).unwrap()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(e) => assert!(
|
|
||||||
false,
|
|
||||||
"There was an error adding the video: {:?}",
|
|
||||||
e
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
25
src/core/ytdl.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use youtube_dl::YoutubeDl;
|
||||||
|
|
||||||
|
pub async fn download_video(
|
||||||
|
url: impl Into<String>,
|
||||||
|
mut output_directory: PathBuf,
|
||||||
|
) -> Result<PathBuf, youtube_dl::Error> {
|
||||||
|
YoutubeDl::new(url)
|
||||||
|
.output_directory(output_directory.to_string_lossy())
|
||||||
|
.output_template("%(title).%(ext)s")
|
||||||
|
.run_async()
|
||||||
|
.await
|
||||||
|
.map(|output| {
|
||||||
|
if let Some(video) = output.into_single_video() {
|
||||||
|
let video_path = format!(
|
||||||
|
"{}.{}",
|
||||||
|
video.title.expect("Should be a title"),
|
||||||
|
video.ext.expect("Should be an extension")
|
||||||
|
);
|
||||||
|
output_directory.push(video_path);
|
||||||
|
};
|
||||||
|
output_directory
|
||||||
|
})
|
||||||
|
}
|
||||||
2292
src/main.rs
Normal file → Executable file
172
src/ui/gst_video.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::num::NonZero;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use cosmic::widget::image::Handle;
|
||||||
|
use iced_video_player::gst_app::prelude::*;
|
||||||
|
use iced_video_player::gst_app::{self};
|
||||||
|
use iced_video_player::{Position, Video, gst};
|
||||||
|
use image::{DynamicImage, ImageFormat, RgbaImage};
|
||||||
|
use tracing::debug;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VideoSettings {
|
||||||
|
pub mute: bool,
|
||||||
|
pub framerate: u16,
|
||||||
|
pub appsink_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VideoSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mute: true,
|
||||||
|
framerate: 60,
|
||||||
|
appsink_name: String::from("lumina_video"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, VideoError>;
|
||||||
|
|
||||||
|
pub fn create_video(url: &Url, settings: &VideoSettings) -> Result<Video> {
|
||||||
|
// Based on `iced_video_player::Video::new`,
|
||||||
|
// but without a text sink so that the built-in subtitle functionality triggers.
|
||||||
|
// and with some better gstreamer tweaks
|
||||||
|
|
||||||
|
gst::init().map_err(VideoError::GlibError)?;
|
||||||
|
|
||||||
|
let pipeline = format!(
|
||||||
|
r#"playbin uri="{0}" video-sink="videoscale ! videoconvert ! videoflip method=automatic ! videorate ! appsink name={1} drop=true caps=video/x-raw,format=NV12,framerate={2}/1,pixel-aspect-ratio=1/1{3}""#,
|
||||||
|
url.as_str(),
|
||||||
|
settings.appsink_name,
|
||||||
|
settings.framerate,
|
||||||
|
if settings.mute { ",mute=true" } else { "" },
|
||||||
|
);
|
||||||
|
|
||||||
|
let pipeline =
|
||||||
|
gst::parse::launch(pipeline.as_ref()).map_err(VideoError::GlibError)?;
|
||||||
|
let pipeline = pipeline
|
||||||
|
.downcast::<gst::Pipeline>()
|
||||||
|
.map_err(|_| VideoError::IcedVideoError(iced_video_player::Error::Cast))?;
|
||||||
|
|
||||||
|
let video_sink: gst::Element = pipeline.property("video-sink");
|
||||||
|
let pad = video_sink.pads().first().cloned().expect("first pad");
|
||||||
|
let pad = pad
|
||||||
|
.dynamic_cast::<gst::GhostPad>()
|
||||||
|
.map_err(|_| VideoError::IcedVideoError(iced_video_player::Error::Cast))?;
|
||||||
|
let bin = pad
|
||||||
|
.parent_element()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
VideoError::IcedVideoError(iced_video_player::Error::AppSink(String::from(
|
||||||
|
"Should have a parent element here",
|
||||||
|
)))
|
||||||
|
})?
|
||||||
|
.downcast::<gst::Bin>()
|
||||||
|
.map_err(|_| VideoError::IcedVideoError(iced_video_player::Error::Cast))?;
|
||||||
|
let video_sink = bin.by_name(&settings.appsink_name).ok_or_else(|| {
|
||||||
|
VideoError::IcedVideoError(iced_video_player::Error::AppSink(format!(
|
||||||
|
"Can't find element {}",
|
||||||
|
settings.appsink_name
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
let video_sink = video_sink
|
||||||
|
.downcast::<gst_app::AppSink>()
|
||||||
|
.map_err(|_| VideoError::IcedVideoError(iced_video_player::Error::Cast))?;
|
||||||
|
Video::from_gst_pipeline(pipeline, video_sink, None)
|
||||||
|
.map_err(VideoError::IcedVideoError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn thumbnail(input: &Url, output: &mut PathBuf) -> Result<Handle> {
|
||||||
|
output.set_extension("png");
|
||||||
|
if output.exists() {
|
||||||
|
let image = image::open(&output).map_err(VideoError::ThumbnailImageError)?;
|
||||||
|
let (width, height, pixels) =
|
||||||
|
(image.width(), image.height(), image.to_rgba8().to_vec());
|
||||||
|
return Ok(Handle::from_rgba(width, height, pixels));
|
||||||
|
}
|
||||||
|
debug!(?output);
|
||||||
|
|
||||||
|
let thumbnails = {
|
||||||
|
let mut video = create_video(input, &VideoSettings::default())?;
|
||||||
|
|
||||||
|
let duration = video.duration();
|
||||||
|
//TODO: how best to decide time?
|
||||||
|
let position = if duration.as_secs_f64() < 20.0 {
|
||||||
|
// If less than 20 seconds, divide duration by 2
|
||||||
|
Position::Time(duration / 2)
|
||||||
|
} else {
|
||||||
|
// If more than 20 seconds, thumbnail at 10 seconds
|
||||||
|
Position::Time(Duration::new(10, 0))
|
||||||
|
};
|
||||||
|
video
|
||||||
|
.thumbnails([position], NonZero::new(1).expect("Not zero"))
|
||||||
|
.map_err(VideoError::IcedVideoError)?
|
||||||
|
};
|
||||||
|
// TODO: do not require clone of pixels data
|
||||||
|
if let Some(cosmic::widget::image::Handle::Rgba {
|
||||||
|
id: _,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pixels,
|
||||||
|
}) = &thumbnails.first()
|
||||||
|
{
|
||||||
|
let image = RgbaImage::from_raw(*width, *height, pixels.to_vec())
|
||||||
|
.map(DynamicImage::ImageRgba8)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
VideoError::ThumbnailError(String::from("Cannot convert handle to image"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !output.exists() {
|
||||||
|
output.set_extension("png");
|
||||||
|
debug!(?output);
|
||||||
|
}
|
||||||
|
|
||||||
|
image
|
||||||
|
.save_with_format(output, ImageFormat::Png)
|
||||||
|
.map_err(VideoError::ThumbnailImageError)?;
|
||||||
|
} else {
|
||||||
|
return Err(VideoError::ThumbnailError(String::from(
|
||||||
|
"Unsupported handle format",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnails
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| VideoError::ThumbnailError(String::from("Error creating handles")))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum VideoError {
|
||||||
|
ThumbnailError(String),
|
||||||
|
IcedVideoError(iced_video_player::Error),
|
||||||
|
GlibError(gst::glib::Error),
|
||||||
|
ThumbnailImageError(image::ImageError),
|
||||||
|
IOError(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for VideoError {}
|
||||||
|
|
||||||
|
impl Display for VideoError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ThumbnailError(message) => {
|
||||||
|
write!(f, "ThumbnailError: {message}")
|
||||||
|
}
|
||||||
|
Self::IcedVideoError(error) => {
|
||||||
|
write!(f, "IcedVideoError: {error}")
|
||||||
|
}
|
||||||
|
Self::GlibError(error) => {
|
||||||
|
write!(f, "GlibError: {error}")
|
||||||
|
}
|
||||||
|
Self::ThumbnailImageError(error) => {
|
||||||
|
write!(f, "ImageError: {error}")
|
||||||
|
}
|
||||||
|
Self::IOError(error) => {
|
||||||
|
write!(f, "IOError: {error}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
use std::{io, path::PathBuf};
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::core::images::Image;
|
use crate::core::images::Image;
|
||||||
use cosmic::{
|
use cosmic::dialog::file_chooser::FileFilter;
|
||||||
Apply, Element, Task,
|
use cosmic::dialog::file_chooser::open::Dialog;
|
||||||
dialog::file_chooser::{FileFilter, open::Dialog},
|
use cosmic::iced::Length;
|
||||||
iced::{Length, alignment::Vertical},
|
use cosmic::iced::alignment::Vertical;
|
||||||
iced_widget::{column, row},
|
use cosmic::iced::widget::{column, row};
|
||||||
theme,
|
use cosmic::widget::space::horizontal;
|
||||||
widget::{
|
use cosmic::widget::{self, Space, button, container, icon, text, text_input};
|
||||||
self, Space, button, container, horizontal_space, icon, text,
|
use cosmic::{Apply, Element, Task, theme};
|
||||||
text_input,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -69,21 +67,14 @@ impl ImageEditor {
|
||||||
return Action::UpdateImage(image);
|
return Action::UpdateImage(image);
|
||||||
}
|
}
|
||||||
Message::PickImage => {
|
Message::PickImage => {
|
||||||
let image_id = self
|
let image_id = self.image.as_ref().map(|v| v.id).unwrap_or_default();
|
||||||
.image
|
let task = Task::perform(pick_image(), move |image_result| {
|
||||||
.as_ref()
|
image_result.map_or(Message::None, |image| {
|
||||||
.map(|v| v.id)
|
let mut image = Image::from(image);
|
||||||
.unwrap_or_default();
|
image.id = image_id;
|
||||||
let task = Task::perform(
|
Message::Update(image)
|
||||||
pick_image(),
|
})
|
||||||
move |image_result| {
|
});
|
||||||
image_result.map_or(Message::None, |image| {
|
|
||||||
let mut image = Image::from(image);
|
|
||||||
image.id = image_id;
|
|
||||||
Message::Update(image)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Action::Task(task);
|
return Action::Task(task);
|
||||||
}
|
}
|
||||||
Message::None => (),
|
Message::None => (),
|
||||||
|
|
@ -94,33 +85,29 @@ impl ImageEditor {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn view(&self) -> Element<Message> {
|
pub fn view(&self) -> Element<Message> {
|
||||||
let container = self.image.as_ref().map_or_else(
|
let container = self.image.as_ref().map_or_else(
|
||||||
|| Space::new(0, 0).apply(container),
|
|| Space::new().apply(container),
|
||||||
|pic| widget::image(pic.path.clone()).apply(container),
|
|pic| widget::image(pic.path.clone()).apply(container),
|
||||||
);
|
);
|
||||||
let column = column![
|
let column = column![self.toolbar(), container.center_x(Length::FillPortion(2))]
|
||||||
self.toolbar(),
|
.spacing(theme::active().cosmic().space_l());
|
||||||
container.center_x(Length::FillPortion(2))
|
|
||||||
]
|
|
||||||
.spacing(theme::active().cosmic().space_l());
|
|
||||||
column.into()
|
column.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toolbar(&self) -> Element<Message> {
|
fn toolbar(&self) -> Element<Message> {
|
||||||
let title_box = text_input("Title...", &self.title)
|
let title_box =
|
||||||
.on_input(Message::ChangeTitle);
|
text_input("Title...", &self.title).on_input(Message::ChangeTitle);
|
||||||
|
|
||||||
let image_selector = button::icon(
|
let image_selector =
|
||||||
icon::from_name("folder-images-symbolic").scale(2),
|
button::icon(icon::from_name("folder-images-symbolic").scale(2))
|
||||||
)
|
.label("Image")
|
||||||
.label("Image")
|
.tooltip("Select a image")
|
||||||
.tooltip("Select a image")
|
.on_press(Message::PickImage)
|
||||||
.on_press(Message::PickImage)
|
.padding(10);
|
||||||
.padding(10);
|
|
||||||
|
|
||||||
row![
|
row![
|
||||||
text::body("Title:"),
|
text::body("Title:"),
|
||||||
title_box,
|
title_box,
|
||||||
horizontal_space(),
|
horizontal(),
|
||||||
image_selector
|
image_selector
|
||||||
]
|
]
|
||||||
.align_y(Vertical::Center)
|
.align_y(Vertical::Center)
|
||||||
|
|
@ -163,9 +150,7 @@ async fn pick_image() -> Result<PathBuf, ImageError> {
|
||||||
error!(?e);
|
error!(?e);
|
||||||
ImageError::DialogClosed
|
ImageError::DialogClosed
|
||||||
})
|
})
|
||||||
.map(|file| {
|
.map(|file| file.url().to_file_path().expect("Should be a file here"))
|
||||||
file.url().to_file_path().expect("Should be a file here")
|
|
||||||
})
|
|
||||||
// rfd::AsyncFileDialog::new()
|
// rfd::AsyncFileDialog::new()
|
||||||
// .set_title("Choose a background...")
|
// .set_title("Choose a background...")
|
||||||
// .add_filter(
|
// .add_filter(
|
||||||
|
|
|
||||||
61
src/ui/image_loader.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use cosmic::widget::image::Handle;
|
||||||
|
use tokio::task::JoinError;
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
pub async fn load_images(path: PathBuf) -> Result<Handle> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let image = image::open(&path).map_err(Error::ImageError)?;
|
||||||
|
let (width, height, pixels) =
|
||||||
|
(image.width(), image.height(), image.to_rgba8().to_vec());
|
||||||
|
Ok(Handle::from_rgba(width, height, pixels))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(Error::AsyncError)
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ImageLoader {
|
||||||
|
decoded_images: HashMap<PathBuf, Handle>,
|
||||||
|
decoding_images: HashSet<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageLoader {
|
||||||
|
pub fn load_image(&mut self, path: &PathBuf) -> Result<Handle> {
|
||||||
|
if self.decoded_images.contains_key(path) {
|
||||||
|
self.decoding_images.remove(path);
|
||||||
|
self.decoded_images
|
||||||
|
.get(path)
|
||||||
|
.ok_or(Error::MissingImage)
|
||||||
|
.cloned()
|
||||||
|
} else {
|
||||||
|
self.decoding_images.insert(path.clone());
|
||||||
|
let image = image::open(path).map_err(Error::ImageError)?;
|
||||||
|
let (width, height, pixels) =
|
||||||
|
(image.width(), image.height(), image.into_bytes());
|
||||||
|
self.decoding_images.remove(path);
|
||||||
|
Ok(Handle::from_rgba(width, height, pixels))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_image(&self, path: &PathBuf) -> Result<Handle> {
|
||||||
|
self.decoded_images
|
||||||
|
.get(path)
|
||||||
|
.ok_or(Error::MissingImage)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
NonImage,
|
||||||
|
AsyncError(JoinError),
|
||||||
|
LoadingError(io::Error),
|
||||||
|
ImageError(image::ImageError),
|
||||||
|
MissingImage,
|
||||||
|
}
|
||||||
1820
src/ui/library.rs
|
|
@ -5,11 +5,12 @@ pub mod image_editor;
|
||||||
pub mod library;
|
pub mod library;
|
||||||
pub mod presentation_editor;
|
pub mod presentation_editor;
|
||||||
pub mod presenter;
|
pub mod presenter;
|
||||||
pub mod service;
|
// pub mod service;
|
||||||
|
pub mod gst_video;
|
||||||
|
pub mod image_loader;
|
||||||
pub mod slide_editor;
|
pub mod slide_editor;
|
||||||
pub mod song_editor;
|
pub mod song_editor;
|
||||||
pub mod text_svg;
|
pub mod text_svg;
|
||||||
pub mod video;
|
|
||||||
pub mod video_editor;
|
pub mod video_editor;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
use std::{
|
use std::collections::HashMap;
|
||||||
collections::HashMap,
|
use std::io;
|
||||||
io,
|
use std::ops::RangeBounds;
|
||||||
ops::RangeBounds,
|
use std::path::{Path, PathBuf};
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::core::presentations::{PresKind, Presentation};
|
use crate::core::presentations::{PresKind, Presentation};
|
||||||
use cosmic::{
|
use crate::ui::widgets::loaded_image::loaded_image;
|
||||||
Element, Task,
|
use cosmic::dialog::file_chooser::FileFilter;
|
||||||
dialog::file_chooser::{FileFilter, open::Dialog},
|
use cosmic::dialog::file_chooser::open::Dialog;
|
||||||
iced::{Background, ContentFit, Length, alignment::Vertical},
|
use cosmic::iced::alignment::Vertical;
|
||||||
iced_widget::{column, row},
|
use cosmic::iced::widget::{column, row};
|
||||||
theme,
|
use cosmic::iced::{Background, ContentFit, Length};
|
||||||
widget::{
|
use cosmic::widget::image::Handle;
|
||||||
self, Space, button, container, context_menu,
|
use cosmic::widget::space::{self, horizontal};
|
||||||
horizontal_space, icon, image::Handle, menu, mouse_area,
|
use cosmic::widget::{
|
||||||
scrollable, text, text_input,
|
self, Space, button, container, context_menu, icon, menu, mouse_area, scrollable,
|
||||||
},
|
text, text_input,
|
||||||
};
|
};
|
||||||
|
use cosmic::{Element, Task, theme};
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
use miette::{IntoDiagnostic, Result, miette};
|
||||||
use mupdf::{Colorspace, Document, Matrix};
|
use mupdf::{Colorspace, Document, Matrix};
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
@ -106,7 +105,7 @@ impl PresentationEditor {
|
||||||
if let PresKind::Pdf {
|
if let PresKind::Pdf {
|
||||||
starting_index,
|
starting_index,
|
||||||
ending_index,
|
ending_index,
|
||||||
} = presentation.kind.clone()
|
} = presentation.kind
|
||||||
{
|
{
|
||||||
let range = starting_index..=ending_index;
|
let range = starting_index..=ending_index;
|
||||||
task = Task::perform(
|
task = Task::perform(
|
||||||
|
|
@ -127,8 +126,7 @@ impl PresentationEditor {
|
||||||
if let Some(presentation) = &self.presentation {
|
if let Some(presentation) = &self.presentation {
|
||||||
let mut presentation = presentation.clone();
|
let mut presentation = presentation.clone();
|
||||||
presentation.title = title;
|
presentation.title = title;
|
||||||
return self
|
return self.update(Message::Update(presentation));
|
||||||
.update(Message::Update(presentation));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Edit(edit) => {
|
Message::Edit(edit) => {
|
||||||
|
|
@ -137,30 +135,20 @@ impl PresentationEditor {
|
||||||
}
|
}
|
||||||
Message::Update(presentation) => {
|
Message::Update(presentation) => {
|
||||||
warn!(?presentation, "about to update");
|
warn!(?presentation, "about to update");
|
||||||
|
self.presentation = Some(presentation.clone());
|
||||||
return Action::UpdatePresentation(presentation);
|
return Action::UpdatePresentation(presentation);
|
||||||
}
|
}
|
||||||
Message::PickPresentation => {
|
Message::PickPresentation => {
|
||||||
let presentation_id = self
|
let presentation_id =
|
||||||
.presentation
|
self.presentation.as_ref().map(|v| v.id).unwrap_or_default();
|
||||||
.as_ref()
|
let task =
|
||||||
.map(|v| v.id)
|
Task::perform(pick_presentation(), move |presentation_result| {
|
||||||
.unwrap_or_default();
|
presentation_result.map_or(Message::None, |presentation| {
|
||||||
let task = Task::perform(
|
let mut presentation = Presentation::from(presentation);
|
||||||
pick_presentation(),
|
presentation.id = presentation_id;
|
||||||
move |presentation_result| {
|
Message::ChangePresentationFile(presentation)
|
||||||
presentation_result.map_or(
|
})
|
||||||
Message::None,
|
});
|
||||||
|presentation| {
|
|
||||||
let mut presentation =
|
|
||||||
Presentation::from(presentation);
|
|
||||||
presentation.id = presentation_id;
|
|
||||||
Message::ChangePresentationFile(
|
|
||||||
presentation,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Action::Task(task);
|
return Action::Task(task);
|
||||||
}
|
}
|
||||||
Message::ChangePresentationFile(presentation) => {
|
Message::ChangePresentationFile(presentation) => {
|
||||||
|
|
@ -185,9 +173,7 @@ impl PresentationEditor {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
task = task.chain(Task::done(Message::Update(
|
task = task.chain(Task::done(Message::Update(presentation.clone())));
|
||||||
presentation.clone(),
|
|
||||||
)));
|
|
||||||
return Action::Task(task);
|
return Action::Task(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -196,13 +182,10 @@ impl PresentationEditor {
|
||||||
}
|
}
|
||||||
Message::None => (),
|
Message::None => (),
|
||||||
Message::NextPage => {
|
Message::NextPage => {
|
||||||
let next_index =
|
let next_index = self.current_slide_index.unwrap_or_default() + 1;
|
||||||
self.current_slide_index.unwrap_or_default() + 1;
|
|
||||||
|
|
||||||
let last_index = if let Some(presentation) =
|
let last_index = if let Some(presentation) = self.presentation.as_ref()
|
||||||
self.presentation.as_ref()
|
&& let PresKind::Pdf { ending_index, .. } = presentation.kind
|
||||||
&& let PresKind::Pdf { ending_index, .. } =
|
|
||||||
presentation.kind
|
|
||||||
{
|
{
|
||||||
ending_index
|
ending_index
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -212,42 +195,31 @@ impl PresentationEditor {
|
||||||
if next_index > last_index {
|
if next_index > last_index {
|
||||||
return Action::None;
|
return Action::None;
|
||||||
}
|
}
|
||||||
self.current_slide =
|
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||||
self.document.as_ref().and_then(|doc| {
|
let page = doc.load_page(next_index).ok()?;
|
||||||
let page = doc.load_page(next_index).ok()?;
|
let matrix = Matrix::IDENTITY;
|
||||||
let matrix = Matrix::IDENTITY;
|
let colorspace = Colorspace::device_rgb();
|
||||||
let colorspace = Colorspace::device_rgb();
|
let Ok(pixmap) = page
|
||||||
let Ok(pixmap) = page
|
.to_pixmap(&matrix, &colorspace, true, true)
|
||||||
.to_pixmap(
|
.into_diagnostic()
|
||||||
&matrix,
|
else {
|
||||||
&colorspace,
|
error!("Can't turn this page into pixmap");
|
||||||
true,
|
return None;
|
||||||
true,
|
};
|
||||||
)
|
debug!(?pixmap);
|
||||||
.into_diagnostic()
|
Some(Handle::from_rgba(
|
||||||
else {
|
pixmap.width(),
|
||||||
error!(
|
pixmap.height(),
|
||||||
"Can't turn this page into pixmap"
|
pixmap.samples().to_vec(),
|
||||||
);
|
))
|
||||||
return None;
|
});
|
||||||
};
|
|
||||||
debug!(?pixmap);
|
|
||||||
Some(Handle::from_rgba(
|
|
||||||
pixmap.width(),
|
|
||||||
pixmap.height(),
|
|
||||||
pixmap.samples().to_vec(),
|
|
||||||
))
|
|
||||||
});
|
|
||||||
self.current_slide_index = Some(next_index);
|
self.current_slide_index = Some(next_index);
|
||||||
}
|
}
|
||||||
Message::PrevPage => {
|
Message::PrevPage => {
|
||||||
let previous_index =
|
let previous_index = self.current_slide_index.unwrap_or_default() - 1;
|
||||||
self.current_slide_index.unwrap_or_default() - 1;
|
|
||||||
|
|
||||||
let first_index = if let Some(presentation) =
|
let first_index = if let Some(presentation) = self.presentation.as_ref()
|
||||||
self.presentation.as_ref()
|
&& let PresKind::Pdf { starting_index, .. } = presentation.kind
|
||||||
&& let PresKind::Pdf { starting_index, .. } =
|
|
||||||
presentation.kind
|
|
||||||
{
|
{
|
||||||
starting_index
|
starting_index
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -257,52 +229,44 @@ impl PresentationEditor {
|
||||||
if previous_index < first_index {
|
if previous_index < first_index {
|
||||||
return Action::None;
|
return Action::None;
|
||||||
}
|
}
|
||||||
self.current_slide =
|
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||||
self.document.as_ref().and_then(|doc| {
|
let page = doc.load_page(previous_index).ok()?;
|
||||||
let page =
|
let matrix = Matrix::IDENTITY;
|
||||||
doc.load_page(previous_index).ok()?;
|
let colorspace = Colorspace::device_rgb();
|
||||||
let matrix = Matrix::IDENTITY;
|
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||||
let colorspace = Colorspace::device_rgb();
|
|
||||||
let pixmap = page
|
|
||||||
.to_pixmap(
|
|
||||||
&matrix,
|
|
||||||
&colorspace,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
Some(Handle::from_rgba(
|
Some(Handle::from_rgba(
|
||||||
pixmap.width(),
|
pixmap.width(),
|
||||||
pixmap.height(),
|
pixmap.height(),
|
||||||
pixmap.samples().to_vec(),
|
pixmap.samples().to_vec(),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
self.current_slide_index = Some(previous_index);
|
self.current_slide_index = Some(previous_index);
|
||||||
}
|
}
|
||||||
Message::ChangeSlide(index) => {
|
Message::ChangeSlide(index) => {
|
||||||
self.current_slide =
|
let starting_index = if let Some(presentation) =
|
||||||
self.document.as_ref().and_then(|doc| {
|
self.presentation.as_ref()
|
||||||
let page = doc
|
&& let PresKind::Pdf { starting_index, .. } = presentation.kind
|
||||||
.load_page(i32::try_from(index).ok()?)
|
{
|
||||||
.ok()?;
|
starting_index
|
||||||
let matrix = Matrix::IDENTITY;
|
} else {
|
||||||
let colorspace = Colorspace::device_rgb();
|
0
|
||||||
let pixmap = page
|
};
|
||||||
.to_pixmap(
|
|
||||||
&matrix,
|
|
||||||
&colorspace,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
Some(Handle::from_rgba(
|
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||||
pixmap.width(),
|
let page = doc
|
||||||
pixmap.height(),
|
.load_page(i32::try_from(index).ok()? + starting_index)
|
||||||
pixmap.samples().to_vec(),
|
.ok()?;
|
||||||
))
|
let matrix = Matrix::IDENTITY;
|
||||||
});
|
let colorspace = Colorspace::device_rgb();
|
||||||
|
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||||
|
|
||||||
|
Some(Handle::from_rgba(
|
||||||
|
pixmap.width(),
|
||||||
|
pixmap.height(),
|
||||||
|
pixmap.samples().to_vec(),
|
||||||
|
))
|
||||||
|
});
|
||||||
self.current_slide_index = i32::try_from(index).ok();
|
self.current_slide_index = i32::try_from(index).ok();
|
||||||
}
|
}
|
||||||
Message::HoverSlide(slide) => {
|
Message::HoverSlide(slide) => {
|
||||||
|
|
@ -315,18 +279,14 @@ impl PresentationEditor {
|
||||||
if let Ok((first, second)) = self.split_before() {
|
if let Ok((first, second)) = self.split_before() {
|
||||||
debug!(?first, ?second);
|
debug!(?first, ?second);
|
||||||
self.update_entire_presentation(&first);
|
self.update_entire_presentation(&first);
|
||||||
return Action::SplitAddPresentation((
|
return Action::SplitAddPresentation((first, second));
|
||||||
first, second,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SplitAfter => {
|
Message::SplitAfter => {
|
||||||
if let Ok((first, second)) = self.split_after() {
|
if let Ok((first, second)) = self.split_after() {
|
||||||
debug!(?first, ?second);
|
debug!(?first, ?second);
|
||||||
self.update_entire_presentation(&first);
|
self.update_entire_presentation(&first);
|
||||||
return Action::SplitAddPresentation((
|
return Action::SplitAddPresentation((first, second));
|
||||||
first, second,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -335,73 +295,62 @@ impl PresentationEditor {
|
||||||
|
|
||||||
pub fn view(&self) -> Element<Message> {
|
pub fn view(&self) -> Element<Message> {
|
||||||
let presentation = self.current_slide.as_ref().map_or_else(
|
let presentation = self.current_slide.as_ref().map_or_else(
|
||||||
|| container(Space::new(0, 0)),
|
|| container(Space::new()),
|
||||||
|slide| {
|
|slide| {
|
||||||
container(
|
container(loaded_image(
|
||||||
widget::image(slide)
|
slide,
|
||||||
.content_fit(ContentFit::ScaleDown),
|
widget::image(slide).content_fit(ContentFit::ScaleDown),
|
||||||
)
|
))
|
||||||
.style(|_| {
|
.style(|_| {
|
||||||
container::background(Background::Color(
|
container::background(Background::Color(cosmic::iced::Color::WHITE))
|
||||||
cosmic::iced::Color::WHITE,
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let pdf_pages: Vec<Element<Message>> =
|
let pdf_pages: Vec<Element<Message>> = self.slides.as_ref().map_or_else(
|
||||||
self.slides.as_ref().map_or_else(
|
|| vec![horizontal().into()],
|
||||||
|| vec![horizontal_space().into()],
|
|pages| {
|
||||||
|pages| {
|
pages
|
||||||
pages
|
.iter()
|
||||||
.iter()
|
.enumerate()
|
||||||
.enumerate()
|
.map(|(index, page)| {
|
||||||
.map(|(index, page)| {
|
let image = loaded_image(
|
||||||
let image = widget::image(page)
|
page,
|
||||||
.height(
|
widget::image(page)
|
||||||
theme::spacing().space_xxxl * 3,
|
.height(theme::spacing().space_xxxl * 3)
|
||||||
)
|
.content_fit(ContentFit::ScaleDown),
|
||||||
.content_fit(ContentFit::ScaleDown);
|
);
|
||||||
let slide = container(image).style(|_| {
|
|
||||||
container::background(Background::Color(
|
let slide = container(image).style(|_| {
|
||||||
cosmic::iced::Color::WHITE,
|
container::background(Background::Color(
|
||||||
))
|
cosmic::iced::Color::WHITE,
|
||||||
});
|
))
|
||||||
let clickable_slide = container(
|
});
|
||||||
mouse_area(slide)
|
let clickable_slide = container(
|
||||||
.on_enter(Message::HoverSlide(
|
mouse_area(slide)
|
||||||
i32::try_from(index).ok(),
|
.on_enter(Message::HoverSlide(i32::try_from(index).ok()))
|
||||||
))
|
.on_exit(Message::HoverSlide(None))
|
||||||
.on_exit(Message::HoverSlide(
|
.on_right_press(Message::ContextMenu(index))
|
||||||
None,
|
.on_press(Message::ChangeSlide(index)),
|
||||||
))
|
)
|
||||||
.on_right_press(
|
.padding(theme::spacing().space_m)
|
||||||
Message::ContextMenu(index),
|
.clip(true)
|
||||||
)
|
.class(self.hovered_slide.map_or(
|
||||||
.on_press(Message::ChangeSlide(
|
theme::Container::Card,
|
||||||
index,
|
|hovered_index| {
|
||||||
)),
|
if i32::try_from(index)
|
||||||
)
|
.is_ok_and(|index| index == hovered_index)
|
||||||
.padding(theme::spacing().space_m)
|
{
|
||||||
.clip(true)
|
theme::Container::Primary
|
||||||
.class(self.hovered_slide.map_or(
|
} else {
|
||||||
theme::Container::Card,
|
theme::Container::Card
|
||||||
|hovered_index| {
|
}
|
||||||
if i32::try_from(index).is_ok_and(
|
},
|
||||||
|index| {
|
));
|
||||||
index == hovered_index
|
clickable_slide.into()
|
||||||
},
|
})
|
||||||
) {
|
.collect()
|
||||||
theme::Container::Primary
|
},
|
||||||
} else {
|
);
|
||||||
theme::Container::Card
|
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
clickable_slide.into()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let pages_column = container(
|
let pages_column = container(
|
||||||
self.context_menu(
|
self.context_menu(
|
||||||
scrollable(
|
scrollable(
|
||||||
|
|
@ -419,14 +368,12 @@ impl PresentationEditor {
|
||||||
]
|
]
|
||||||
.spacing(theme::spacing().space_xxl);
|
.spacing(theme::spacing().space_xxl);
|
||||||
let control_buttons = row![
|
let control_buttons = row![
|
||||||
button::standard("Previous Page")
|
button::standard("Previous Page").on_press(Message::PrevPage),
|
||||||
.on_press(Message::PrevPage),
|
space::horizontal(),
|
||||||
horizontal_space(),
|
|
||||||
button::standard("Next Page").on_press(Message::NextPage),
|
button::standard("Next Page").on_press(Message::NextPage),
|
||||||
];
|
];
|
||||||
let column =
|
let column = column![self.toolbar(), main_row, control_buttons]
|
||||||
column![self.toolbar(), main_row, control_buttons]
|
.spacing(theme::active().cosmic().space_l());
|
||||||
.spacing(theme::active().cosmic().space_l());
|
|
||||||
column.into()
|
column.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,18 +386,17 @@ impl PresentationEditor {
|
||||||
)
|
)
|
||||||
.on_input(Message::ChangeTitle);
|
.on_input(Message::ChangeTitle);
|
||||||
|
|
||||||
let presentation_selector = button::icon(
|
let presentation_selector =
|
||||||
icon::from_name("folder-presentations-symbolic").scale(2),
|
button::icon(icon::from_name("folder-presentations-symbolic").scale(2))
|
||||||
)
|
.label("Change Presentation")
|
||||||
.label("Change Presentation")
|
.tooltip("Select a presentation")
|
||||||
.tooltip("Select a presentation")
|
.on_press(Message::PickPresentation)
|
||||||
.on_press(Message::PickPresentation)
|
.padding(10);
|
||||||
.padding(10);
|
|
||||||
|
|
||||||
row![
|
row![
|
||||||
text::body("Title:"),
|
text::body("Title:"),
|
||||||
title_box,
|
title_box,
|
||||||
horizontal_space(),
|
space::horizontal(),
|
||||||
presentation_selector
|
presentation_selector
|
||||||
]
|
]
|
||||||
.align_y(Vertical::Center)
|
.align_y(Vertical::Center)
|
||||||
|
|
@ -462,17 +408,13 @@ impl PresentationEditor {
|
||||||
self.editing
|
self.editing
|
||||||
}
|
}
|
||||||
|
|
||||||
fn context_menu<'b>(
|
fn context_menu<'b>(&self, items: Element<'b, Message>) -> Element<'b, Message> {
|
||||||
&self,
|
const SPLIT_ABOVE_ICON: &[u8] = include_bytes!("../../res/icons/split-above.svg");
|
||||||
items: Element<'b, Message>,
|
const SPLIT_BELOW_ICON: &[u8] = include_bytes!("../../res/icons/split-below.svg");
|
||||||
) -> Element<'b, Message> {
|
|
||||||
if self.context_menu_id.is_some() {
|
if self.context_menu_id.is_some() {
|
||||||
let before_icon =
|
let before_icon = icon::from_svg_bytes(SPLIT_ABOVE_ICON).symbolic(true);
|
||||||
icon::from_path("./res/split-above.svg".into())
|
let after_icon = icon::from_svg_bytes(SPLIT_BELOW_ICON).symbolic(true);
|
||||||
.symbolic(true);
|
|
||||||
let after_icon =
|
|
||||||
icon::from_path("./res/split-below.svg".into())
|
|
||||||
.symbolic(true);
|
|
||||||
let menu_items = vec![
|
let menu_items = vec![
|
||||||
menu::Item::Button(
|
menu::Item::Button(
|
||||||
"Spit Before",
|
"Spit Before",
|
||||||
|
|
@ -489,9 +431,7 @@ impl PresentationEditor {
|
||||||
items,
|
items,
|
||||||
self.context_menu_id.map_or_else(
|
self.context_menu_id.map_or_else(
|
||||||
|| None,
|
|| None,
|
||||||
|_| {
|
|_| Some(menu::items(&HashMap::new(), menu_items)),
|
||||||
Some(menu::items(&HashMap::new(), menu_items))
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Element::from(context_menu)
|
Element::from(context_menu)
|
||||||
|
|
@ -500,60 +440,45 @@ impl PresentationEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_entire_presentation(
|
fn update_entire_presentation(&mut self, presentation: &Presentation) {
|
||||||
&mut self,
|
|
||||||
presentation: &Presentation,
|
|
||||||
) {
|
|
||||||
self.presentation = Some(presentation.clone());
|
self.presentation = Some(presentation.clone());
|
||||||
self.title.clone_from(&presentation.title);
|
self.title.clone_from(&presentation.title);
|
||||||
self.document =
|
self.document =
|
||||||
Document::open(&presentation.path.as_path()).ok();
|
Document::open(presentation.path.to_str().unwrap_or_default()).ok();
|
||||||
self.page_count = self
|
self.page_count = self.document.as_ref().and_then(|doc| doc.page_count().ok());
|
||||||
.document
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|doc| doc.page_count().ok());
|
|
||||||
warn!("changing presentation");
|
warn!("changing presentation");
|
||||||
let pages = if let PresKind::Pdf {
|
let pages = if let PresKind::Pdf {
|
||||||
starting_index,
|
starting_index,
|
||||||
ending_index,
|
ending_index,
|
||||||
} = presentation.kind
|
} = presentation.kind
|
||||||
{
|
{
|
||||||
self.current_slide =
|
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||||
self.document.as_ref().and_then(|doc| {
|
let page = doc.load_page(starting_index).ok()?;
|
||||||
let page = doc.load_page(starting_index).ok()?;
|
let matrix = Matrix::IDENTITY;
|
||||||
let matrix = Matrix::IDENTITY;
|
let colorspace = Colorspace::device_rgb();
|
||||||
let colorspace = Colorspace::device_rgb();
|
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||||
let pixmap = page
|
|
||||||
.to_pixmap(&matrix, &colorspace, true, true)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
Some(Handle::from_rgba(
|
Some(Handle::from_rgba(
|
||||||
pixmap.width(),
|
pixmap.width(),
|
||||||
pixmap.height(),
|
pixmap.height(),
|
||||||
pixmap.samples().to_vec(),
|
pixmap.samples().to_vec(),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
self.current_slide_index = Some(starting_index);
|
self.current_slide_index = Some(starting_index);
|
||||||
get_pages(
|
get_pages(starting_index..=ending_index, presentation.path.clone())
|
||||||
starting_index..=ending_index,
|
|
||||||
presentation.path.clone(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
self.current_slide =
|
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||||
self.document.as_ref().and_then(|doc| {
|
let page = doc.load_page(0).ok()?;
|
||||||
let page = doc.load_page(0).ok()?;
|
let matrix = Matrix::IDENTITY;
|
||||||
let matrix = Matrix::IDENTITY;
|
let colorspace = Colorspace::device_rgb();
|
||||||
let colorspace = Colorspace::device_rgb();
|
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||||
let pixmap = page
|
|
||||||
.to_pixmap(&matrix, &colorspace, true, true)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
Some(Handle::from_rgba(
|
Some(Handle::from_rgba(
|
||||||
pixmap.width(),
|
pixmap.width(),
|
||||||
pixmap.height(),
|
pixmap.height(),
|
||||||
pixmap.samples().to_vec(),
|
pixmap.samples().to_vec(),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
self.current_slide_index = Some(0);
|
self.current_slide_index = Some(0);
|
||||||
get_pages(.., presentation.path.clone())
|
get_pages(.., presentation.path.clone())
|
||||||
};
|
};
|
||||||
|
|
@ -562,12 +487,8 @@ impl PresentationEditor {
|
||||||
|
|
||||||
fn split_before(&self) -> Result<(Presentation, Presentation)> {
|
fn split_before(&self) -> Result<(Presentation, Presentation)> {
|
||||||
if let Some(index) = self.context_menu_id {
|
if let Some(index) = self.context_menu_id {
|
||||||
let Some(current_presentation) =
|
let Some(current_presentation) = self.presentation.as_ref() else {
|
||||||
self.presentation.as_ref()
|
return Err(miette!("There is no current presentation"));
|
||||||
else {
|
|
||||||
return Err(miette!(
|
|
||||||
"There is no current presentation"
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
let first_presentation = Presentation {
|
let first_presentation = Presentation {
|
||||||
id: current_presentation.id,
|
id: current_presentation.id,
|
||||||
|
|
@ -580,23 +501,22 @@ impl PresentationEditor {
|
||||||
},
|
},
|
||||||
_ => current_presentation.kind.clone(),
|
_ => current_presentation.kind.clone(),
|
||||||
},
|
},
|
||||||
|
created_at: current_presentation.created_at,
|
||||||
|
accessed_at: current_presentation.accessed_at,
|
||||||
};
|
};
|
||||||
let second_presentation = Presentation {
|
let second_presentation = Presentation {
|
||||||
id: 0,
|
id: 0,
|
||||||
title: format!(
|
title: format!("{} (2)", current_presentation.title.clone()),
|
||||||
"{} (2)",
|
|
||||||
current_presentation.title.clone()
|
|
||||||
),
|
|
||||||
path: current_presentation.path.clone(),
|
path: current_presentation.path.clone(),
|
||||||
kind: match current_presentation.kind {
|
kind: match current_presentation.kind {
|
||||||
PresKind::Pdf { ending_index, .. } => {
|
PresKind::Pdf { ending_index, .. } => PresKind::Pdf {
|
||||||
PresKind::Pdf {
|
starting_index: index,
|
||||||
starting_index: index,
|
ending_index,
|
||||||
ending_index,
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => current_presentation.kind.clone(),
|
_ => current_presentation.kind.clone(),
|
||||||
},
|
},
|
||||||
|
created_at: current_presentation.created_at,
|
||||||
|
accessed_at: current_presentation.accessed_at,
|
||||||
};
|
};
|
||||||
Ok((first_presentation, second_presentation))
|
Ok((first_presentation, second_presentation))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -609,12 +529,8 @@ impl PresentationEditor {
|
||||||
|
|
||||||
fn split_after(&self) -> Result<(Presentation, Presentation)> {
|
fn split_after(&self) -> Result<(Presentation, Presentation)> {
|
||||||
if let Some(index) = self.context_menu_id {
|
if let Some(index) = self.context_menu_id {
|
||||||
let Some(current_presentation) =
|
let Some(current_presentation) = self.presentation.as_ref() else {
|
||||||
self.presentation.as_ref()
|
return Err(miette!("There is no current presentation"));
|
||||||
else {
|
|
||||||
return Err(miette!(
|
|
||||||
"There is no current presentation"
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
let first_presentation = Presentation {
|
let first_presentation = Presentation {
|
||||||
id: current_presentation.id,
|
id: current_presentation.id,
|
||||||
|
|
@ -627,23 +543,22 @@ impl PresentationEditor {
|
||||||
},
|
},
|
||||||
_ => current_presentation.kind.clone(),
|
_ => current_presentation.kind.clone(),
|
||||||
},
|
},
|
||||||
|
created_at: current_presentation.created_at,
|
||||||
|
accessed_at: current_presentation.accessed_at,
|
||||||
};
|
};
|
||||||
let second_presentation = Presentation {
|
let second_presentation = Presentation {
|
||||||
id: 0,
|
id: 0,
|
||||||
title: format!(
|
title: format!("{} (2)", current_presentation.title.clone()),
|
||||||
"{} (2)",
|
|
||||||
current_presentation.title.clone()
|
|
||||||
),
|
|
||||||
path: current_presentation.path.clone(),
|
path: current_presentation.path.clone(),
|
||||||
kind: match current_presentation.kind {
|
kind: match current_presentation.kind {
|
||||||
PresKind::Pdf { ending_index, .. } => {
|
PresKind::Pdf { ending_index, .. } => PresKind::Pdf {
|
||||||
PresKind::Pdf {
|
starting_index: index + 1,
|
||||||
starting_index: index + 1,
|
ending_index,
|
||||||
ending_index,
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => current_presentation.kind.clone(),
|
_ => current_presentation.kind.clone(),
|
||||||
},
|
},
|
||||||
|
created_at: current_presentation.created_at,
|
||||||
|
accessed_at: current_presentation.accessed_at,
|
||||||
};
|
};
|
||||||
Ok((first_presentation, second_presentation))
|
Ok((first_presentation, second_presentation))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -665,23 +580,23 @@ fn get_pages(
|
||||||
range: impl RangeBounds<i32>,
|
range: impl RangeBounds<i32>,
|
||||||
presentation_path: impl AsRef<Path>,
|
presentation_path: impl AsRef<Path>,
|
||||||
) -> Option<Vec<Handle>> {
|
) -> Option<Vec<Handle>> {
|
||||||
let document = Document::open(presentation_path.as_ref()).ok()?;
|
let document =
|
||||||
|
Document::open(presentation_path.as_ref().to_str().unwrap_or_default()).ok()?;
|
||||||
let pages = document.pages().ok()?;
|
let pages = document.pages().ok()?;
|
||||||
Some(
|
Some(
|
||||||
pages
|
pages
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(index, page)| {
|
.filter_map(|(index, page)| {
|
||||||
if !range.contains(&i32::try_from(index).expect(
|
if !range.contains(
|
||||||
"looking for a pdf index that is way too large",
|
&i32::try_from(index)
|
||||||
)) {
|
.expect("looking for a pdf index that is way too large"),
|
||||||
|
) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let page = page.ok()?;
|
let page = page.ok()?;
|
||||||
let matrix = Matrix::IDENTITY;
|
let matrix = Matrix::IDENTITY;
|
||||||
let colorspace = Colorspace::device_rgb();
|
let colorspace = Colorspace::device_rgb();
|
||||||
let pixmap = page
|
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||||
.to_pixmap(&matrix, &colorspace, true, true)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
Some(Handle::from_rgba(
|
Some(Handle::from_rgba(
|
||||||
pixmap.width(),
|
pixmap.width(),
|
||||||
|
|
@ -707,9 +622,7 @@ async fn pick_presentation() -> Result<PathBuf, PresentationError> {
|
||||||
error!(?e);
|
error!(?e);
|
||||||
PresentationError::DialogClosed
|
PresentationError::DialogClosed
|
||||||
})
|
})
|
||||||
.map(|file| {
|
.map(|file| file.url().to_file_path().expect("Should be a file here"))
|
||||||
file.url().to_file_path().expect("Should be a file here")
|
|
||||||
})
|
|
||||||
// rfd::AsyncFileDialog::new()
|
// rfd::AsyncFileDialog::new()
|
||||||
// .set_title("Choose a background...")
|
// .set_title("Choose a background...")
|
||||||
// .add_filter(
|
// .add_filter(
|
||||||
|
|
|
||||||
1512
src/ui/presenter.rs
|
|
@ -3,14 +3,14 @@ use cosmic::iced::Size;
|
||||||
use cosmic::iced_core::widget::tree;
|
use cosmic::iced_core::widget::tree;
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
Element,
|
Element,
|
||||||
|
iced::core::{
|
||||||
|
self, Clipboard, Shell, layout, renderer, widget::Tree,
|
||||||
|
},
|
||||||
iced::{
|
iced::{
|
||||||
Event, Length, Point, Rectangle, Vector,
|
Event, Length, Point, Rectangle, Vector,
|
||||||
clipboard::dnd::{DndEvent, SourceEvent},
|
clipboard::dnd::{DndEvent, SourceEvent},
|
||||||
event, mouse,
|
event, mouse,
|
||||||
},
|
},
|
||||||
iced_core::{
|
|
||||||
self, Clipboard, Shell, layout, renderer, widget::Tree,
|
|
||||||
},
|
|
||||||
widget::Widget,
|
widget::Widget,
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
use std::{io, path::PathBuf};
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::Renderer;
|
||||||
Renderer,
|
use cosmic::iced::{Color, Font, Length, Size};
|
||||||
iced::{Color, Font, Length, Size},
|
use cosmic::widget::canvas::{self, Program, Stroke};
|
||||||
widget::{
|
use cosmic::widget::{self, container};
|
||||||
self,
|
|
||||||
canvas::{self, Program, Stroke},
|
|
||||||
container,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
|
@ -59,10 +55,7 @@ struct EditorProgram {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlideEditor {
|
impl SlideEditor {
|
||||||
pub fn view(
|
pub fn view(&self, _font: Font) -> cosmic::Element<'_, SlideWidget> {
|
||||||
&self,
|
|
||||||
_font: Font,
|
|
||||||
) -> cosmic::Element<'_, SlideWidget> {
|
|
||||||
container(
|
container(
|
||||||
widget::canvas(&self.program)
|
widget::canvas(&self.program)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|
@ -75,9 +68,7 @@ impl SlideEditor {
|
||||||
/// Ensure to use the `cosmic::Theme and cosmic::Renderer` here
|
/// Ensure to use the `cosmic::Theme and cosmic::Renderer` here
|
||||||
/// or else it will not compile
|
/// or else it will not compile
|
||||||
#[allow(clippy::extra_unused_lifetimes)]
|
#[allow(clippy::extra_unused_lifetimes)]
|
||||||
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer> for EditorProgram {
|
||||||
for EditorProgram
|
|
||||||
{
|
|
||||||
type State = ();
|
type State = ();
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
|
|
@ -86,7 +77,7 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
_theme: &cosmic::Theme,
|
_theme: &cosmic::Theme,
|
||||||
bounds: cosmic::iced::Rectangle,
|
bounds: cosmic::iced::Rectangle,
|
||||||
_cursor: cosmic::iced_core::mouse::Cursor,
|
_cursor: cosmic::iced::core::mouse::Cursor,
|
||||||
) -> Vec<canvas::Geometry<Renderer>> {
|
) -> Vec<canvas::Geometry<Renderer>> {
|
||||||
// We prepare a new `Frame`
|
// We prepare a new `Frame`
|
||||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||||
|
|
@ -103,15 +94,11 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
frame.fill(&circle, Color::BLACK);
|
frame.fill(&circle, Color::BLACK);
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
&circle,
|
&circle,
|
||||||
Stroke::default()
|
Stroke::default().with_width(5.0).with_color(Color::BLACK),
|
||||||
.with_width(5.0)
|
|
||||||
.with_color(Color::BLACK),
|
|
||||||
);
|
);
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
&border,
|
&border,
|
||||||
Stroke::default()
|
Stroke::default().with_width(5.0).with_color(Color::BLACK),
|
||||||
.with_width(5.0)
|
|
||||||
.with_color(Color::BLACK),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then, we produce the geometry
|
// Then, we produce the geometry
|
||||||
|
|
@ -121,10 +108,10 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
fn update(
|
fn update(
|
||||||
&self,
|
&self,
|
||||||
_state: &mut Self::State,
|
_state: &mut Self::State,
|
||||||
event: canvas::Event,
|
event: &canvas::Event,
|
||||||
bounds: cosmic::iced::Rectangle,
|
bounds: cosmic::iced::Rectangle,
|
||||||
_cursor: cosmic::iced_core::mouse::Cursor,
|
_cursor: cosmic::iced::core::mouse::Cursor,
|
||||||
) -> (canvas::event::Status, Option<SlideWidget>) {
|
) -> Option<cosmic::iced::widget::Action<SlideWidget>> {
|
||||||
match event {
|
match event {
|
||||||
canvas::Event::Mouse(event) => match event {
|
canvas::Event::Mouse(event) => match event {
|
||||||
cosmic::iced::mouse::Event::CursorEntered => {
|
cosmic::iced::mouse::Event::CursorEntered => {
|
||||||
|
|
@ -133,9 +120,7 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
cosmic::iced::mouse::Event::CursorLeft => {
|
cosmic::iced::mouse::Event::CursorLeft => {
|
||||||
debug!("cursor left");
|
debug!("cursor left");
|
||||||
}
|
}
|
||||||
cosmic::iced::mouse::Event::CursorMoved {
|
cosmic::iced::mouse::Event::CursorMoved { position } => {
|
||||||
position,
|
|
||||||
} => {
|
|
||||||
if bounds.x < position.x
|
if bounds.x < position.x
|
||||||
&& bounds.y < position.y
|
&& bounds.y < position.y
|
||||||
&& (bounds.width + bounds.x) > position.x
|
&& (bounds.width + bounds.x) > position.x
|
||||||
|
|
@ -148,25 +133,32 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||||
// self.mouse_button_pressed = Some(button);
|
// self.mouse_button_pressed = Some(button);
|
||||||
debug!(?button, "mouse button pressed");
|
debug!(?button, "mouse button pressed");
|
||||||
}
|
}
|
||||||
cosmic::iced::mouse::Event::ButtonReleased(
|
cosmic::iced::mouse::Event::ButtonReleased(button) => {
|
||||||
button,
|
debug!(?button, "mouse button released");
|
||||||
) => debug!(?button, "mouse button released"),
|
}
|
||||||
cosmic::iced::mouse::Event::WheelScrolled {
|
cosmic::iced::mouse::Event::WheelScrolled { delta } => {
|
||||||
delta,
|
debug!(?delta, "scroll wheel");
|
||||||
} => debug!(?delta, "scroll wheel"),
|
}
|
||||||
},
|
},
|
||||||
canvas::Event::Touch(_event) => debug!("test"),
|
canvas::Event::Touch(_event) => debug!("test"),
|
||||||
canvas::Event::Keyboard(_event) => debug!("test"),
|
canvas::Event::Keyboard(_event) => debug!("test"),
|
||||||
|
canvas::Event::Window(_event) => todo!(),
|
||||||
|
canvas::Event::InputMethod(_event) => todo!(),
|
||||||
|
// canvas::Event::A11y(_id, _action_request) => todo!(),
|
||||||
|
canvas::Event::Dnd(_dnd_event) => todo!(),
|
||||||
|
canvas::Event::PlatformSpecific(_platform_specific) => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(canvas::event::Status::Ignored, None)
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
_state: &Self::State,
|
_state: &Self::State,
|
||||||
_bounds: cosmic::iced::Rectangle,
|
_bounds: cosmic::iced::Rectangle,
|
||||||
_cursor: cosmic::iced_core::mouse::Cursor,
|
_cursor: cosmic::iced::core::mouse::Cursor,
|
||||||
) -> cosmic::iced_core::mouse::Interaction {
|
) -> cosmic::iced::core::mouse::Interaction {
|
||||||
cosmic::iced_core::mouse::Interaction::default()
|
cosmic::iced::core::mouse::Interaction::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1908
src/ui/song_editor.rs
Normal file → Executable file
|
|
@ -1,31 +1,26 @@
|
||||||
use std::{
|
use std::fmt::{Display, Write};
|
||||||
fmt::{Display, Write},
|
use std::fs;
|
||||||
fs,
|
use std::hash::{Hash, Hasher};
|
||||||
hash::{Hash, Hasher},
|
use std::path::PathBuf;
|
||||||
path::PathBuf,
|
use std::sync::Arc;
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::cosmic_theme::palette::rgb::Rgba;
|
||||||
cosmic_theme::palette::{IntoColor, Srgb, rgb::Rgba},
|
use cosmic::cosmic_theme::palette::{IntoColor, Srgb};
|
||||||
iced::{
|
use cosmic::iced::font::{Style, Weight};
|
||||||
ContentFit, Length, Size,
|
use cosmic::iced::{ContentFit, Length, Size};
|
||||||
font::{Style, Weight},
|
use cosmic::prelude::*;
|
||||||
},
|
use cosmic::widget::image::Handle;
|
||||||
prelude::*,
|
use cosmic::widget::{Image, Space};
|
||||||
widget::{Image, Space, image::Handle},
|
|
||||||
};
|
|
||||||
use derive_more::Debug;
|
use derive_more::Debug;
|
||||||
use miette::{IntoDiagnostic, Result, miette};
|
use miette::{IntoDiagnostic, Result, miette};
|
||||||
use rapidhash::v3::rapidhash_v3;
|
use rapidhash::v3::rapidhash_v3;
|
||||||
use resvg::{
|
use resvg::tiny_skia::{self, Pixmap};
|
||||||
tiny_skia::{self, Pixmap},
|
use resvg::usvg::{Tree, fontdb};
|
||||||
usvg::{Tree, fontdb},
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{TextAlignment, core::slide::Slide};
|
use crate::TextAlignment;
|
||||||
|
use crate::core::slide::Slide;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct TextSvg {
|
pub struct TextSvg {
|
||||||
|
|
@ -68,9 +63,7 @@ impl Hash for TextSvg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Font {
|
pub struct Font {
|
||||||
name: String,
|
name: String,
|
||||||
weight: Weight,
|
weight: Weight,
|
||||||
|
|
@ -78,9 +71,7 @@ pub struct Font {
|
||||||
size: u8,
|
size: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize)]
|
||||||
Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Shadow {
|
pub struct Shadow {
|
||||||
pub offset_x: i16,
|
pub offset_x: i16,
|
||||||
pub offset_y: i16,
|
pub offset_y: i16,
|
||||||
|
|
@ -88,9 +79,7 @@ pub struct Shadow {
|
||||||
pub color: Color,
|
pub color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize)]
|
||||||
Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Stroke {
|
pub struct Stroke {
|
||||||
size: u16,
|
size: u16,
|
||||||
color: Color,
|
color: Color,
|
||||||
|
|
@ -107,9 +96,7 @@ impl From<cosmic::font::Font> for Font {
|
||||||
fn from(value: cosmic::font::Font) -> Self {
|
fn from(value: cosmic::font::Font) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: match value.family {
|
name: match value.family {
|
||||||
cosmic::iced::font::Family::Name(name) => {
|
cosmic::iced::font::Family::Name(name) => name.to_string(),
|
||||||
name.to_string()
|
|
||||||
}
|
|
||||||
_ => "Quicksand Bold".into(),
|
_ => "Quicksand Bold".into(),
|
||||||
},
|
},
|
||||||
size: 20,
|
size: 20,
|
||||||
|
|
@ -235,10 +222,7 @@ impl Default for Color {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Color {
|
impl Display for Color {
|
||||||
fn fmt(
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
&self,
|
|
||||||
f: &mut std::fmt::Formatter<'_>,
|
|
||||||
) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.to_css_hex_string())
|
write!(f, "{}", self.to_css_hex_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -291,23 +275,15 @@ impl TextSvg {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn alignment(
|
pub const fn alignment(mut self, alignment: TextAlignment) -> Self {
|
||||||
mut self,
|
|
||||||
alignment: TextAlignment,
|
|
||||||
) -> Self {
|
|
||||||
self.alignment = alignment;
|
self.alignment = alignment;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn build(
|
pub fn build(mut self, size: Size, mut cache: Option<PathBuf>) -> Self {
|
||||||
mut self,
|
|
||||||
size: Size,
|
|
||||||
mut cache: Option<PathBuf>,
|
|
||||||
) -> Self {
|
|
||||||
// debug!("starting...");
|
// debug!("starting...");
|
||||||
|
|
||||||
let mut final_svg = String::with_capacity(1024);
|
let mut final_svg = String::with_capacity(1024);
|
||||||
|
|
@ -322,55 +298,47 @@ impl TextSvg {
|
||||||
let center_y = (size.width / 2.0).to_string();
|
let center_y = (size.width / 2.0).to_string();
|
||||||
let x_width_padded = (size.width - 10.0).to_string();
|
let x_width_padded = (size.width - 10.0).to_string();
|
||||||
|
|
||||||
let (text_anchor, starting_y_position, text_x_position) =
|
let (text_anchor, starting_y_position, text_x_position) = match self.alignment {
|
||||||
match self.alignment {
|
TextAlignment::TopLeft => ("start", font_size, "10"),
|
||||||
TextAlignment::TopLeft => ("start", font_size, "10"),
|
TextAlignment::TopCenter => ("middle", font_size, center_y.as_str()),
|
||||||
TextAlignment::TopCenter => {
|
TextAlignment::TopRight => ("end", font_size, x_width_padded.as_str()),
|
||||||
("middle", font_size, center_y.as_str())
|
TextAlignment::MiddleLeft => {
|
||||||
}
|
let middle_position = size.height / 2.0;
|
||||||
TextAlignment::TopRight => {
|
let position = half_lines
|
||||||
("end", font_size, x_width_padded.as_str())
|
.mul_add(-text_and_line_spacing, middle_position)
|
||||||
}
|
+ text_and_line_spacing / 2.0;
|
||||||
TextAlignment::MiddleLeft => {
|
("start", position, "10")
|
||||||
let middle_position = size.height / 2.0;
|
}
|
||||||
let position = half_lines.mul_add(
|
TextAlignment::MiddleCenter => {
|
||||||
-text_and_line_spacing,
|
let middle_position = size.height / 2.0;
|
||||||
middle_position,
|
let position = half_lines
|
||||||
);
|
.mul_add(-text_and_line_spacing, middle_position)
|
||||||
("start", position, "10")
|
+ text_and_line_spacing / 2.0;
|
||||||
}
|
("middle", position, center_y.as_str())
|
||||||
TextAlignment::MiddleCenter => {
|
}
|
||||||
let middle_position = size.height / 2.0;
|
TextAlignment::MiddleRight => {
|
||||||
let position = half_lines.mul_add(
|
let middle_position = size.height / 2.0;
|
||||||
-text_and_line_spacing,
|
let position = half_lines
|
||||||
middle_position,
|
.mul_add(-text_and_line_spacing, middle_position)
|
||||||
);
|
+ text_and_line_spacing / 2.0;
|
||||||
("middle", position, center_y.as_str())
|
("end", position, x_width_padded.as_str())
|
||||||
}
|
}
|
||||||
TextAlignment::MiddleRight => {
|
TextAlignment::BottomLeft => {
|
||||||
let middle_position = size.height / 2.0;
|
let position =
|
||||||
let position = half_lines.mul_add(
|
(total_lines as f32).mul_add(-text_and_line_spacing, size.height);
|
||||||
-text_and_line_spacing,
|
("start", position, "10")
|
||||||
middle_position,
|
}
|
||||||
);
|
TextAlignment::BottomCenter => {
|
||||||
("end", position, x_width_padded.as_str())
|
let position =
|
||||||
}
|
(total_lines as f32).mul_add(-text_and_line_spacing, size.height);
|
||||||
TextAlignment::BottomLeft => {
|
("middle", position, center_y.as_str())
|
||||||
let position = (total_lines as f32)
|
}
|
||||||
.mul_add(-text_and_line_spacing, size.height);
|
TextAlignment::BottomRight => {
|
||||||
("start", position, "10")
|
let position =
|
||||||
}
|
(total_lines as f32).mul_add(-text_and_line_spacing, size.height);
|
||||||
TextAlignment::BottomCenter => {
|
("end", position, x_width_padded.as_str())
|
||||||
let position = (total_lines as f32)
|
}
|
||||||
.mul_add(-text_and_line_spacing, size.height);
|
};
|
||||||
("middle", position, center_y.as_str())
|
|
||||||
}
|
|
||||||
TextAlignment::BottomRight => {
|
|
||||||
let position = (total_lines as f32)
|
|
||||||
.mul_add(-text_and_line_spacing, size.height);
|
|
||||||
("end", position, x_width_padded.as_str())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let font_style = match self.font.style {
|
let font_style = match self.font.style {
|
||||||
Style::Normal => "normal",
|
Style::Normal => "normal",
|
||||||
|
|
@ -379,9 +347,7 @@ impl TextSvg {
|
||||||
};
|
};
|
||||||
|
|
||||||
let font_weight = match self.font.weight {
|
let font_weight = match self.font.weight {
|
||||||
Weight::Thin | Weight::ExtraLight | Weight::Light => {
|
Weight::Thin | Weight::ExtraLight | Weight::Light => "lighter",
|
||||||
"lighter"
|
|
||||||
}
|
|
||||||
Weight::Normal | Weight::Medium => "normal",
|
Weight::Normal | Weight::Medium => "normal",
|
||||||
Weight::Semibold | Weight::Bold => "bold",
|
Weight::Semibold | Weight::Bold => "bold",
|
||||||
Weight::ExtraBold | Weight::Black => "bolder",
|
Weight::ExtraBold | Weight::Black => "bolder",
|
||||||
|
|
@ -397,10 +363,7 @@ impl TextSvg {
|
||||||
let _ = write!(
|
let _ = write!(
|
||||||
final_svg,
|
final_svg,
|
||||||
"<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
"<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
||||||
shadow.offset_x,
|
shadow.offset_x, shadow.offset_y, shadow.spread, shadow.color
|
||||||
shadow.offset_y,
|
|
||||||
shadow.spread,
|
|
||||||
shadow.color
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final_svg.push_str("</defs>");
|
final_svg.push_str("</defs>");
|
||||||
|
|
@ -439,10 +402,7 @@ impl TextSvg {
|
||||||
let _ = write!(
|
let _ = write!(
|
||||||
final_svg,
|
final_svg,
|
||||||
"<tspan x=\"0\" y=\"{}\">{}</tspan>",
|
"<tspan x=\"0\" y=\"{}\">{}</tspan>",
|
||||||
(index as f32).mul_add(
|
(index as f32).mul_add(text_and_line_spacing, starting_y_position),
|
||||||
text_and_line_spacing,
|
|
||||||
starting_y_position
|
|
||||||
),
|
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -489,11 +449,9 @@ impl TextSvg {
|
||||||
let transform = tiny_skia::Transform::default();
|
let transform = tiny_skia::Transform::default();
|
||||||
|
|
||||||
#[allow(clippy::cast_sign_loss)]
|
#[allow(clippy::cast_sign_loss)]
|
||||||
let (size_width, size_height) =
|
let (size_width, size_height) = (size.width as u32, size.height as u32);
|
||||||
(size.width as u32, size.height as u32);
|
|
||||||
|
|
||||||
let Some(mut pixmap) = Pixmap::new(size_width, size_height)
|
let Some(mut pixmap) = Pixmap::new(size_width, size_height) else {
|
||||||
else {
|
|
||||||
error!("Couldn't create a new pixmap from size");
|
error!("Couldn't create a new pixmap from size");
|
||||||
return self;
|
return self;
|
||||||
};
|
};
|
||||||
|
|
@ -509,8 +467,7 @@ impl TextSvg {
|
||||||
|
|
||||||
// debug!("saved");
|
// debug!("saved");
|
||||||
// let handle = Handle::from_path(path);
|
// let handle = Handle::from_path(path);
|
||||||
let handle =
|
let handle = Handle::from_rgba(size_width, size_height, pixmap.take());
|
||||||
Handle::from_rgba(size_width, size_height, pixmap.take());
|
|
||||||
self.handle = Some(handle);
|
self.handle = Some(handle);
|
||||||
// debug!("stored");
|
// debug!("stored");
|
||||||
self
|
self
|
||||||
|
|
@ -518,7 +475,7 @@ impl TextSvg {
|
||||||
|
|
||||||
pub fn view<'a>(&self) -> Element<'a, Message> {
|
pub fn view<'a>(&self) -> Element<'a, Message> {
|
||||||
self.handle.clone().map_or_else(
|
self.handle.clone().map_or_else(
|
||||||
|| Element::from(Space::new(Length::Fill, Length::Fill)),
|
|| Element::from(Space::new().height(Length::Fill).width(Length::Fill)),
|
||||||
|handle| {
|
|handle| {
|
||||||
Image::new(handle)
|
Image::new(handle)
|
||||||
.content_fit(ContentFit::Cover)
|
.content_fit(ContentFit::Cover)
|
||||||
|
|
@ -581,9 +538,7 @@ pub fn text_svg_generator_with_cache(
|
||||||
let font = slide.font().unwrap_or_default();
|
let font = slide.font().unwrap_or_default();
|
||||||
let text_svg = TextSvg::new(slide.text())
|
let text_svg = TextSvg::new(slide.text())
|
||||||
.alignment(slide.text_alignment())
|
.alignment(slide.text_alignment())
|
||||||
.fill(
|
.fill(slide.text_color().unwrap_or_else(|| "#fff".into()));
|
||||||
slide.text_color().unwrap_or_else(|| "#fff".into()),
|
|
||||||
);
|
|
||||||
let text_svg = if let Some(stroke) = slide.stroke() {
|
let text_svg = if let Some(stroke) = slide.stroke() {
|
||||||
text_svg.stroke(stroke)
|
text_svg.stroke(stroke)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -596,8 +551,7 @@ pub fn text_svg_generator_with_cache(
|
||||||
};
|
};
|
||||||
let text_svg = text_svg.font(font).fontdb(Arc::clone(fontdb));
|
let text_svg = text_svg.font(font).fontdb(Arc::clone(fontdb));
|
||||||
// debug!(fill = ?text_svg.fill, font = ?text_svg.font, stroke = ?text_svg.stroke, shadow = ?text_svg.shadow, text = ?text_svg.text);
|
// debug!(fill = ?text_svg.fill, font = ?text_svg.font, stroke = ?text_svg.stroke, shadow = ?text_svg.shadow, text = ?text_svg.text);
|
||||||
let text_svg =
|
let text_svg = text_svg.build(Size::new(1280.0, 720.0), cache);
|
||||||
text_svg.build(Size::new(1280.0, 720.0), cache);
|
|
||||||
slide.text_svg = Some(text_svg);
|
slide.text_svg = Some(text_svg);
|
||||||
Ok(slide)
|
Ok(slide)
|
||||||
}
|
}
|
||||||
|
|
@ -637,10 +591,10 @@ mod tests {
|
||||||
slide
|
slide
|
||||||
.text_svg
|
.text_svg
|
||||||
.is_some_and(|svg| svg.handle.is_some())
|
.is_some_and(|svg| svg.handle.is_some())
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
Err(e) => assert!(false, "There was an issue creating the TextSvg: {e}"),
|
Err(e) => panic!("There was an issue creating the TextSvg: {e}"),
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
// use iced_video_player::Video;
|
|
||||||
|
|
||||||
// fn video_player(video: &Video) -> Element<Message> {}
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
use std::{io, path::PathBuf};
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::dialog::file_chooser::FileFilter;
|
||||||
Element, Task,
|
use cosmic::dialog::file_chooser::open::Dialog;
|
||||||
dialog::file_chooser::{FileFilter, open::Dialog},
|
use cosmic::iced::Length;
|
||||||
iced::{Length, alignment::Vertical},
|
use cosmic::iced::alignment::Vertical;
|
||||||
iced_widget::{column, row},
|
use cosmic::iced::widget::{column, row};
|
||||||
theme,
|
use cosmic::prelude::*;
|
||||||
widget::{
|
use cosmic::widget::space::{self, horizontal};
|
||||||
Space, button, container, horizontal_space, icon,
|
use cosmic::widget::{Space, button, container, icon, slider, text, text_input};
|
||||||
progress_bar, text, text_input,
|
use cosmic::{Element, Task, theme};
|
||||||
},
|
use iced_video_player::{Position, Video, VideoPlayer};
|
||||||
};
|
|
||||||
use iced_video_player::{Video, VideoPlayer};
|
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::core::videos;
|
use crate::core::videos;
|
||||||
|
use crate::ui::gst_video;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VideoEditor {
|
pub struct VideoEditor {
|
||||||
|
|
@ -23,6 +23,7 @@ pub struct VideoEditor {
|
||||||
core_video: Option<videos::Video>,
|
core_video: Option<videos::Video>,
|
||||||
title: String,
|
title: String,
|
||||||
editing: bool,
|
editing: bool,
|
||||||
|
position: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
|
|
@ -41,6 +42,8 @@ pub enum Message {
|
||||||
None,
|
None,
|
||||||
PauseVideo,
|
PauseVideo,
|
||||||
UpdateVideoFile(videos::Video),
|
UpdateVideoFile(videos::Video),
|
||||||
|
VideoPos(f64),
|
||||||
|
NewFrame,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoEditor {
|
impl VideoEditor {
|
||||||
|
|
@ -51,6 +54,7 @@ impl VideoEditor {
|
||||||
core_video: None,
|
core_video: None,
|
||||||
title: "Death was Arrested".to_string(),
|
title: "Death was Arrested".to_string(),
|
||||||
editing: false,
|
editing: false,
|
||||||
|
position: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn update(&mut self, message: Message) -> Action {
|
pub fn update(&mut self, message: Message) -> Action {
|
||||||
|
|
@ -80,29 +84,41 @@ impl VideoEditor {
|
||||||
warn!(?video);
|
warn!(?video);
|
||||||
return Action::UpdateVideo(video);
|
return Action::UpdateVideo(video);
|
||||||
}
|
}
|
||||||
|
Message::VideoPos(position) => {
|
||||||
|
if let Some(video) = self.video.as_mut() {
|
||||||
|
let pausing = video.paused();
|
||||||
|
video.set_paused(true);
|
||||||
|
let position =
|
||||||
|
Position::Time(std::time::Duration::from_secs_f64(position));
|
||||||
|
if let Err(e) = video.seek(position, false) {
|
||||||
|
error!(?e);
|
||||||
|
}
|
||||||
|
video.set_paused(pausing);
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::PickVideo => {
|
Message::PickVideo => {
|
||||||
let video_id = self
|
let video_id = self.core_video.as_ref().map(|v| v.id).unwrap_or_default();
|
||||||
.core_video
|
let task = Task::perform(pick_video(), move |video_result| {
|
||||||
.as_ref()
|
video_result.map_or(Message::None, |video| {
|
||||||
.map(|v| v.id)
|
let mut video = videos::Video::from(video);
|
||||||
.unwrap_or_default();
|
video.id = video_id;
|
||||||
let task = Task::perform(
|
Message::UpdateVideoFile(video)
|
||||||
pick_video(),
|
})
|
||||||
move |video_result| {
|
});
|
||||||
video_result.map_or(Message::None, |video| {
|
|
||||||
let mut video =
|
|
||||||
videos::Video::from(video);
|
|
||||||
video.id = video_id;
|
|
||||||
Message::UpdateVideoFile(video)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return Action::Task(task);
|
return Action::Task(task);
|
||||||
}
|
}
|
||||||
Message::UpdateVideoFile(video) => {
|
Message::UpdateVideoFile(video) => {
|
||||||
self.update_entire_video(&video);
|
self.update_entire_video(&video);
|
||||||
return Action::UpdateVideo(video);
|
return Action::UpdateVideo(video);
|
||||||
}
|
}
|
||||||
|
Message::NewFrame => {
|
||||||
|
if let Some(video) = &self.video
|
||||||
|
&& self.position > 0.0
|
||||||
|
&& video.position().as_secs_f64() != 0.0
|
||||||
|
{
|
||||||
|
self.position = video.position().as_secs_f64();
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::None => (),
|
Message::None => (),
|
||||||
}
|
}
|
||||||
Action::None
|
Action::None
|
||||||
|
|
@ -110,20 +126,22 @@ impl VideoEditor {
|
||||||
|
|
||||||
pub fn view(&self) -> Element<Message> {
|
pub fn view(&self) -> Element<Message> {
|
||||||
let video_elements = self.video.as_ref().map_or_else(
|
let video_elements = self.video.as_ref().map_or_else(
|
||||||
|| container(horizontal_space()),
|
|| container(horizontal()),
|
||||||
|video| {
|
|video| {
|
||||||
let play_button = button::icon(if video.paused() {
|
let play_button = button::icon(if video.paused() {
|
||||||
icon::from_name("media-playback-start")
|
icon::from_name("media-playback-start-symbolic")
|
||||||
} else {
|
} else {
|
||||||
icon::from_name("media-playback-pause")
|
icon::from_name("media-playback-pause-symbolic")
|
||||||
})
|
})
|
||||||
.on_press(Message::PauseVideo);
|
.on_press(Message::PauseVideo);
|
||||||
let video_track = progress_bar(
|
let video_track = slider(
|
||||||
0.0..=video.duration().as_secs_f32(),
|
0.0..=video.duration().as_secs_f64(),
|
||||||
video.position().as_secs_f32(),
|
video.position().as_secs_f64(),
|
||||||
|
Message::VideoPos,
|
||||||
)
|
)
|
||||||
.height(cosmic::theme::spacing().space_s)
|
.step(0.1)
|
||||||
.width(Length::Fill);
|
.width(Length::Fill)
|
||||||
|
.height(cosmic::theme::spacing().space_s);
|
||||||
container(
|
container(
|
||||||
row![play_button, video_track]
|
row![play_button, video_track]
|
||||||
.align_y(Vertical::Center)
|
.align_y(Vertical::Center)
|
||||||
|
|
@ -134,10 +152,18 @@ impl VideoEditor {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let video_player = self.video.as_ref().map_or_else(
|
let video_player = self
|
||||||
|| Element::from(Space::new(0, 0)),
|
.video
|
||||||
|video| Element::from(VideoPlayer::new(video)),
|
.as_ref()
|
||||||
);
|
.map_or_else(
|
||||||
|
|| Space::new().apply(container),
|
||||||
|
|video| {
|
||||||
|
VideoPlayer::new(video)
|
||||||
|
.on_new_frame(Message::NewFrame)
|
||||||
|
.apply(container)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.center(Length::Fill);
|
||||||
|
|
||||||
let video_section = column![video_player, video_elements]
|
let video_section = column![video_player, video_elements]
|
||||||
.spacing(cosmic::theme::spacing().space_s);
|
.spacing(cosmic::theme::spacing().space_s);
|
||||||
|
|
@ -150,21 +176,20 @@ impl VideoEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toolbar(&self) -> Element<Message> {
|
fn toolbar(&self) -> Element<Message> {
|
||||||
let title_box = text_input("Title...", &self.title)
|
let title_box =
|
||||||
.on_input(Message::ChangeTitle);
|
text_input("Title...", &self.title).on_input(Message::ChangeTitle);
|
||||||
|
|
||||||
let video_selector = button::icon(
|
let video_selector =
|
||||||
icon::from_name("folder-videos-symbolic").scale(2),
|
button::icon(icon::from_name("folder-videos-symbolic").scale(2))
|
||||||
)
|
.label("Video")
|
||||||
.label("Video")
|
.tooltip("Select a video")
|
||||||
.tooltip("Select a video")
|
.on_press(Message::PickVideo)
|
||||||
.on_press(Message::PickVideo)
|
.padding(10);
|
||||||
.padding(10);
|
|
||||||
|
|
||||||
row![
|
row![
|
||||||
text::body("Title:"),
|
text::body("Title:"),
|
||||||
title_box,
|
title_box,
|
||||||
horizontal_space(),
|
space::horizontal(),
|
||||||
video_selector
|
video_selector
|
||||||
]
|
]
|
||||||
.align_y(Vertical::Center)
|
.align_y(Vertical::Center)
|
||||||
|
|
@ -177,15 +202,27 @@ impl VideoEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_entire_video(&mut self, video: &videos::Video) {
|
fn update_entire_video(&mut self, video: &videos::Video) {
|
||||||
let Ok(mut player_video) =
|
debug!(?video);
|
||||||
Url::from_file_path(video.path.clone())
|
let Ok(url) = Url::from_file_path(video.path.clone()) else {
|
||||||
.map(|url| Video::new(&url).expect("Should be here"))
|
|
||||||
else {
|
|
||||||
self.video = None;
|
self.video = None;
|
||||||
self.title.clone_from(&video.title);
|
self.title.clone_from(&video.title);
|
||||||
self.core_video = Some(video.clone());
|
self.core_video = Some(video.clone());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let settings = gst_video::VideoSettings {
|
||||||
|
mute: false,
|
||||||
|
framerate: 60,
|
||||||
|
appsink_name: "lumina_video".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(mut player_video) = gst_video::create_video(&url, &settings) else {
|
||||||
|
self.video = None;
|
||||||
|
self.title = format!("{}: {}", String::from("Video Missing"), &video.title);
|
||||||
|
self.core_video = Some(video.clone());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
player_video.set_paused(true);
|
player_video.set_paused(true);
|
||||||
self.video = Some(player_video);
|
self.video = Some(player_video);
|
||||||
self.title.clone_from(&video.title);
|
self.title.clone_from(&video.title);
|
||||||
|
|
@ -214,9 +251,7 @@ async fn pick_video() -> Result<PathBuf, VideoError> {
|
||||||
error!(?e);
|
error!(?e);
|
||||||
VideoError::DialogClosed
|
VideoError::DialogClosed
|
||||||
})
|
})
|
||||||
.map(|file| {
|
.map(|file| file.url().to_file_path().expect("Should be a file here"))
|
||||||
file.url().to_file_path().expect("Should be a file here")
|
|
||||||
})
|
|
||||||
// rfd::AsyncFileDialog::new()
|
// rfd::AsyncFileDialog::new()
|
||||||
// .set_title("Choose a background...")
|
// .set_title("Choose a background...")
|
||||||
// .add_filter(
|
// .add_filter(
|
||||||
|
|
|
||||||
|
|
@ -27,19 +27,16 @@ use cosmic::iced::advanced::layout::{self, Layout};
|
||||||
use cosmic::iced::advanced::widget::{Operation, Tree, Widget, tree};
|
use cosmic::iced::advanced::widget::{Operation, Tree, Widget, tree};
|
||||||
use cosmic::iced::advanced::{Clipboard, Shell, overlay, renderer};
|
use cosmic::iced::advanced::{Clipboard, Shell, overlay, renderer};
|
||||||
use cosmic::iced::alignment::{self, Alignment};
|
use cosmic::iced::alignment::{self, Alignment};
|
||||||
use cosmic::iced::event::{self, Event};
|
use cosmic::iced::event::Event;
|
||||||
use cosmic::iced::{self, Transformation, mouse};
|
|
||||||
use cosmic::iced::{
|
use cosmic::iced::{
|
||||||
Background, Border, Color, Element, Length, Padding, Pixels,
|
self, Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle,
|
||||||
Point, Rectangle, Size, Vector,
|
Size, Transformation, Vector, mouse,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Action, DragEvent, DropPosition};
|
use super::{Action, DragEvent, DropPosition};
|
||||||
|
|
||||||
pub fn column<'a, Message, Theme, Renderer>(
|
pub fn column<'a, Message, Theme, Renderer>(
|
||||||
children: impl IntoIterator<
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Column<'a, Message, Theme, Renderer>
|
) -> Column<'a, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
|
|
@ -73,12 +70,8 @@ const DRAG_DEADBAND_DISTANCE: f32 = 5.0;
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Column<
|
pub struct Column<'a, Message, Theme = cosmic::Theme, Renderer = iced::Renderer>
|
||||||
'a,
|
where
|
||||||
Message,
|
|
||||||
Theme = cosmic::Theme,
|
|
||||||
Renderer = iced::Renderer,
|
|
||||||
> where
|
|
||||||
Theme: Catalog,
|
Theme: Catalog,
|
||||||
{
|
{
|
||||||
spacing: f32,
|
spacing: f32,
|
||||||
|
|
@ -94,8 +87,7 @@ pub struct Column<
|
||||||
class: Theme::Class<'a>,
|
class: Theme::Class<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer>
|
impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer>
|
||||||
Column<'a, Message, Theme, Renderer>
|
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
Theme: Catalog,
|
Theme: Catalog,
|
||||||
|
|
@ -114,9 +106,7 @@ where
|
||||||
|
|
||||||
/// Creates a [`Column`] with the given elements.
|
/// Creates a [`Column`] with the given elements.
|
||||||
pub fn with_children(
|
pub fn with_children(
|
||||||
children: impl IntoIterator<
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let iterator = children.into_iter();
|
let iterator = children.into_iter();
|
||||||
|
|
||||||
|
|
@ -131,9 +121,7 @@ where
|
||||||
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||||
/// call [`Column::width`] or [`Column::height`] accordingly.
|
/// call [`Column::width`] or [`Column::height`] accordingly.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn from_vec(
|
pub fn from_vec(children: Vec<Element<'a, Message, Theme, Renderer>>) -> Self {
|
||||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
spacing: 0.0,
|
spacing: 0.0,
|
||||||
padding: Padding::ZERO,
|
padding: Padding::ZERO,
|
||||||
|
|
@ -184,10 +172,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
||||||
pub fn align_x(
|
pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
|
||||||
mut self,
|
|
||||||
align: impl Into<alignment::Horizontal>,
|
|
||||||
) -> Self {
|
|
||||||
self.align = Alignment::from(align.into());
|
self.align = Alignment::from(align.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -223,9 +208,7 @@ where
|
||||||
/// Adds an element to the [`Column`], if `Some`.
|
/// Adds an element to the [`Column`], if `Some`.
|
||||||
pub fn push_maybe(
|
pub fn push_maybe(
|
||||||
self,
|
self,
|
||||||
child: Option<
|
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||||
impl Into<Element<'a, Message, Theme, Renderer>>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
if let Some(child) = child {
|
if let Some(child) = child {
|
||||||
self.push(child)
|
self.push(child)
|
||||||
|
|
@ -236,10 +219,7 @@ where
|
||||||
|
|
||||||
/// Sets the style of the [`Column`].
|
/// Sets the style of the [`Column`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn style(
|
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||||
mut self,
|
|
||||||
style: impl Fn(&Theme) -> Style + 'a,
|
|
||||||
) -> Self
|
|
||||||
where
|
where
|
||||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
{
|
{
|
||||||
|
|
@ -249,10 +229,7 @@ where
|
||||||
|
|
||||||
/// Sets the style class of the [`Column`].
|
/// Sets the style class of the [`Column`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn class(
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
mut self,
|
|
||||||
class: impl Into<Theme::Class<'a>>,
|
|
||||||
) -> Self {
|
|
||||||
self.class = class.into();
|
self.class = class.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -260,18 +237,13 @@ where
|
||||||
/// Extends the [`Column`] with the given children.
|
/// Extends the [`Column`] with the given children.
|
||||||
pub fn extend(
|
pub fn extend(
|
||||||
self,
|
self,
|
||||||
children: impl IntoIterator<
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
children.into_iter().fold(self, Self::push)
|
children.into_iter().fold(self, Self::push)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The message produced by the [`Column`] when a child is dragged.
|
/// The message produced by the [`Column`] when a child is dragged.
|
||||||
pub fn on_drag(
|
pub fn on_drag(mut self, on_reorder: impl Fn(DragEvent) -> Message + 'a) -> Self {
|
||||||
mut self,
|
|
||||||
on_reorder: impl Fn(DragEvent) -> Message + 'a,
|
|
||||||
) -> Self {
|
|
||||||
self.on_drag = Some(Box::new(on_reorder));
|
self.on_drag = Some(Box::new(on_reorder));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -320,8 +292,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Message, Renderer> Default
|
impl<Message, Renderer> Default for Column<'_, Message, Theme, Renderer>
|
||||||
for Column<'_, Message, Theme, Renderer>
|
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
Theme: Catalog,
|
Theme: Catalog,
|
||||||
|
|
@ -337,9 +308,7 @@ impl<'a, Message, Theme, Renderer: renderer::Renderer>
|
||||||
where
|
where
|
||||||
Theme: Catalog,
|
Theme: Catalog,
|
||||||
{
|
{
|
||||||
fn from_iter<
|
fn from_iter<T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>>(
|
||||||
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
|
||||||
>(
|
|
||||||
iter: T,
|
iter: T,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::with_children(iter)
|
Self::with_children(iter)
|
||||||
|
|
@ -376,7 +345,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
limits: &layout::Limits,
|
limits: &layout::Limits,
|
||||||
|
|
@ -392,74 +361,77 @@ where
|
||||||
self.padding,
|
self.padding,
|
||||||
self.spacing,
|
self.spacing,
|
||||||
self.align,
|
self.align,
|
||||||
&self.children,
|
self.children.as_mut(),
|
||||||
&mut tree.children,
|
&mut tree.children,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn operate(
|
fn operate(
|
||||||
&self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
operation: &mut dyn Operation,
|
operation: &mut dyn Operation,
|
||||||
) {
|
) {
|
||||||
operation.container(
|
operation.container(None, layout.bounds());
|
||||||
None,
|
|
||||||
layout.bounds(),
|
operation.traverse(&mut |operation| {
|
||||||
&mut |operation| {
|
self.children
|
||||||
self.children
|
.iter_mut()
|
||||||
.iter()
|
.zip(&mut tree.children)
|
||||||
.zip(&mut tree.children)
|
.zip(layout.children())
|
||||||
.zip(layout.children())
|
.for_each(|((child, state), c_layout)| {
|
||||||
.for_each(|((child, state), c_layout)| {
|
child.as_widget_mut().operate(
|
||||||
child.as_widget().operate(
|
state,
|
||||||
state,
|
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||||
c_layout.with_virtual_offset(
|
renderer,
|
||||||
layout.virtual_offset(),
|
operation,
|
||||||
),
|
);
|
||||||
renderer,
|
});
|
||||||
operation,
|
});
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
event: Event,
|
event: &Event,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
clipboard: &mut dyn Clipboard,
|
clipboard: &mut dyn Clipboard,
|
||||||
shell: &mut Shell<'_, Message>,
|
shell: &mut Shell<'_, Message>,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) -> event::Status {
|
) {
|
||||||
let mut event_status = event::Status::Ignored;
|
|
||||||
|
|
||||||
let action = tree.state.downcast_mut::<Action>();
|
let action = tree.state.downcast_mut::<Action>();
|
||||||
|
|
||||||
|
// let children have precedence
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.for_each(|((child, state), c_layout)| {
|
||||||
|
child.as_widget_mut().update(
|
||||||
|
state,
|
||||||
|
&event.clone(),
|
||||||
|
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::Mouse(mouse::Event::ButtonPressed(
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
mouse::Button::Left,
|
if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
|
||||||
)) => {
|
for (index, child_layout) in layout.children().enumerate() {
|
||||||
if let Some(cursor_position) =
|
if child_layout.bounds().contains(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 {
|
*action = Action::Picking {
|
||||||
index,
|
index,
|
||||||
origin: cursor_position,
|
origin: cursor_position,
|
||||||
};
|
};
|
||||||
event_status = event::Status::Captured;
|
shell.capture_event();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -468,10 +440,8 @@ where
|
||||||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||||
match *action {
|
match *action {
|
||||||
Action::Picking { index, origin } => {
|
Action::Picking { index, origin } => {
|
||||||
if let Some(cursor_position) =
|
if let Some(cursor_position) = cursor.position()
|
||||||
cursor.position()
|
&& cursor_position.distance(origin) > self.deadband_zone
|
||||||
&& cursor_position.distance(origin)
|
|
||||||
> self.deadband_zone
|
|
||||||
{
|
{
|
||||||
// Start dragging
|
// Start dragging
|
||||||
*action = Action::Dragging {
|
*action = Action::Dragging {
|
||||||
|
|
@ -480,66 +450,44 @@ where
|
||||||
last_cursor: cursor_position,
|
last_cursor: cursor_position,
|
||||||
};
|
};
|
||||||
if let Some(on_reorder) = &self.on_drag {
|
if let Some(on_reorder) = &self.on_drag {
|
||||||
shell.publish(on_reorder(
|
shell.publish(on_reorder(DragEvent::Picked { index }));
|
||||||
DragEvent::Picked { index },
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
event_status = event::Status::Captured;
|
shell.capture_event();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Dragging { origin, index, .. } => {
|
Action::Dragging { origin, index, .. } => {
|
||||||
if let Some(cursor_position) =
|
if let Some(cursor_position) = cursor.position() {
|
||||||
cursor.position()
|
|
||||||
{
|
|
||||||
*action = Action::Dragging {
|
*action = Action::Dragging {
|
||||||
last_cursor: cursor_position,
|
last_cursor: cursor_position,
|
||||||
origin,
|
origin,
|
||||||
index,
|
index,
|
||||||
};
|
};
|
||||||
event_status = event::Status::Captured;
|
shell.capture_event();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Mouse(mouse::Event::ButtonReleased(
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||||
mouse::Button::Left,
|
|
||||||
)) => {
|
|
||||||
match *action {
|
match *action {
|
||||||
Action::Dragging { index, .. } => {
|
Action::Dragging { index, .. } => {
|
||||||
if let Some(cursor_position) =
|
if let Some(cursor_position) = cursor.position() {
|
||||||
cursor.position()
|
|
||||||
{
|
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
if bounds.contains(cursor_position) {
|
if bounds.contains(cursor_position) {
|
||||||
let (target_index, drop_position) =
|
let (target_index, drop_position) = self
|
||||||
self.compute_target_index(
|
.compute_target_index(cursor_position, layout, index);
|
||||||
cursor_position,
|
|
||||||
layout,
|
|
||||||
index,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(on_reorder) =
|
if let Some(on_reorder) = &self.on_drag {
|
||||||
&self.on_drag
|
shell.publish(on_reorder(DragEvent::Dropped {
|
||||||
{
|
index,
|
||||||
shell.publish(on_reorder(
|
target_index,
|
||||||
DragEvent::Dropped {
|
drop_position,
|
||||||
index,
|
}));
|
||||||
target_index,
|
shell.capture_event();
|
||||||
drop_position,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
event_status =
|
|
||||||
event::Status::Captured;
|
|
||||||
}
|
}
|
||||||
} else if let Some(on_reorder) =
|
} else if let Some(on_reorder) = &self.on_drag {
|
||||||
&self.on_drag
|
shell.publish(on_reorder(DragEvent::Canceled { index }));
|
||||||
{
|
shell.capture_event();
|
||||||
shell.publish(on_reorder(
|
|
||||||
DragEvent::Canceled { index },
|
|
||||||
));
|
|
||||||
event_status =
|
|
||||||
event::Status::Captured;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*action = Action::Idle;
|
*action = Action::Idle;
|
||||||
|
|
@ -553,28 +501,6 @@ where
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
fn mouse_interaction(
|
||||||
|
|
@ -598,8 +524,7 @@ where
|
||||||
.map(|((child, state), c_layout)| {
|
.map(|((child, state), c_layout)| {
|
||||||
child.as_widget().mouse_interaction(
|
child.as_widget().mouse_interaction(
|
||||||
state,
|
state,
|
||||||
c_layout
|
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||||
.with_virtual_offset(layout.virtual_offset()),
|
|
||||||
cursor,
|
cursor,
|
||||||
viewport,
|
viewport,
|
||||||
renderer,
|
renderer,
|
||||||
|
|
@ -633,20 +558,15 @@ where
|
||||||
|
|
||||||
// Determine the target index based on cursor position
|
// Determine the target index based on cursor position
|
||||||
let target_index = if cursor.position().is_some() {
|
let target_index = if cursor.position().is_some() {
|
||||||
let (target_index, _) = self
|
let (target_index, _) =
|
||||||
.compute_target_index(
|
self.compute_target_index(*last_cursor, layout, *index);
|
||||||
*last_cursor,
|
|
||||||
layout,
|
|
||||||
*index,
|
|
||||||
);
|
|
||||||
target_index.min(child_count - 1)
|
target_index.min(child_count - 1)
|
||||||
} else {
|
} else {
|
||||||
*index
|
*index
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store the width of the dragged item
|
// Store the width of the dragged item
|
||||||
let drag_bounds =
|
let drag_bounds = layout.children().nth(*index).unwrap().bounds();
|
||||||
layout.children().nth(*index).unwrap().bounds();
|
|
||||||
let drag_height = drag_bounds.height + self.spacing;
|
let drag_height = drag_bounds.height + self.spacing;
|
||||||
|
|
||||||
// Draw all children except the one being dragged
|
// Draw all children except the one being dragged
|
||||||
|
|
@ -654,125 +574,92 @@ where
|
||||||
for i in 0..child_count {
|
for i in 0..child_count {
|
||||||
let child = &self.children[i];
|
let child = &self.children[i];
|
||||||
let state = &tree.children[i];
|
let state = &tree.children[i];
|
||||||
let child_layout =
|
let child_layout = layout.children().nth(i).unwrap();
|
||||||
layout.children().nth(i).unwrap();
|
|
||||||
|
|
||||||
// Draw the dragged item separately
|
// Draw the dragged item separately
|
||||||
// TODO: Draw a shadow below the picked item to enhance the
|
// TODO: Draw a shadow below the picked item to enhance the
|
||||||
// floating effect
|
// floating effect
|
||||||
if i == *index {
|
if i == *index {
|
||||||
let scaling =
|
let scaling = Transformation::scale(style.scale);
|
||||||
Transformation::scale(style.scale);
|
let translation = *last_cursor - *origin * scaling;
|
||||||
let translation =
|
renderer.with_translation(translation, |renderer| {
|
||||||
*last_cursor - *origin * scaling;
|
renderer.with_transformation(scaling, |renderer| {
|
||||||
renderer.with_translation(
|
renderer.with_layer(child_layout.bounds(), |renderer| {
|
||||||
translation,
|
child.as_widget().draw(
|
||||||
|renderer| {
|
state,
|
||||||
renderer.with_transformation(
|
renderer,
|
||||||
scaling,
|
theme,
|
||||||
|renderer| {
|
default_style,
|
||||||
renderer.with_layer(
|
child_layout,
|
||||||
child_layout.bounds(),
|
cursor,
|
||||||
|renderer| {
|
viewport,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} 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,
|
||||||
|
};
|
||||||
|
|
||||||
// Keep track of the total translation so we can
|
let translation = Vector::new(0.0, offset as f32 * drag_height);
|
||||||
// draw the "ghost" of the dragged item later
|
renderer.with_translation(translation, |renderer| {
|
||||||
translations -= (child_layout
|
child.as_widget().draw(
|
||||||
.bounds()
|
state,
|
||||||
.height
|
renderer,
|
||||||
+ self.spacing)
|
theme,
|
||||||
* offset.signum() as f32;
|
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
|
// Draw a ghost of the dragged item in its would-be position
|
||||||
let ghost_translation =
|
let ghost_translation = Vector::new(0.0, translations);
|
||||||
Vector::new(0.0, translations);
|
renderer.with_translation(ghost_translation, |renderer| {
|
||||||
renderer.with_translation(
|
renderer.fill_quad(
|
||||||
ghost_translation,
|
renderer::Quad {
|
||||||
|renderer| {
|
bounds: drag_bounds,
|
||||||
renderer.fill_quad(
|
border: style.ghost_border,
|
||||||
renderer::Quad {
|
..renderer::Quad::default()
|
||||||
bounds: drag_bounds,
|
},
|
||||||
border: style.ghost_border,
|
style.ghost_background,
|
||||||
..renderer::Quad::default()
|
);
|
||||||
},
|
});
|
||||||
style.ghost_background,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Draw all children normally when not dragging
|
// Draw all children normally when not dragging
|
||||||
if let Some(clipped_viewport) =
|
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||||
layout.bounds().intersection(viewport)
|
|
||||||
{
|
|
||||||
let viewport = if self.clip {
|
let viewport = if self.clip {
|
||||||
&clipped_viewport
|
&clipped_viewport
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -783,18 +670,14 @@ where
|
||||||
.iter()
|
.iter()
|
||||||
.zip(&tree.children)
|
.zip(&tree.children)
|
||||||
.zip(layout.children())
|
.zip(layout.children())
|
||||||
.filter(|(_, layout)| {
|
.filter(|(_, layout)| layout.bounds().intersects(viewport))
|
||||||
layout.bounds().intersects(viewport)
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
child.as_widget().draw(
|
child.as_widget().draw(
|
||||||
state,
|
state,
|
||||||
renderer,
|
renderer,
|
||||||
theme,
|
theme,
|
||||||
default_style,
|
default_style,
|
||||||
c_layout.with_virtual_offset(
|
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||||
layout.virtual_offset(),
|
|
||||||
),
|
|
||||||
cursor,
|
cursor,
|
||||||
viewport,
|
viewport,
|
||||||
);
|
);
|
||||||
|
|
@ -807,8 +690,9 @@ where
|
||||||
fn overlay<'b>(
|
fn overlay<'b>(
|
||||||
&'b mut self,
|
&'b mut self,
|
||||||
tree: &'b mut Tree,
|
tree: &'b mut Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'b>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
overlay::from_children(
|
overlay::from_children(
|
||||||
|
|
@ -816,6 +700,7 @@ where
|
||||||
tree,
|
tree,
|
||||||
layout,
|
layout,
|
||||||
renderer,
|
renderer,
|
||||||
|
viewport,
|
||||||
translation,
|
translation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -825,7 +710,7 @@ where
|
||||||
state: &Tree,
|
state: &Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
dnd_rectangles: &mut cosmic::iced_core::clipboard::DndDestinationRectangles,
|
dnd_rectangles: &mut cosmic::iced::core::clipboard::DndDestinationRectangles,
|
||||||
) {
|
) {
|
||||||
for ((e, c_layout), state) in self
|
for ((e, c_layout), state) in self
|
||||||
.children
|
.children
|
||||||
|
|
@ -843,8 +728,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer>
|
impl<'a, Message, Theme, Renderer> From<Column<'a, Message, Theme, Renderer>>
|
||||||
From<Column<'a, Message, Theme, Renderer>>
|
|
||||||
for Element<'a, Message, Theme, Renderer>
|
for Element<'a, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Message: 'a,
|
Message: 'a,
|
||||||
|
|
@ -900,8 +784,7 @@ impl Catalog for cosmic::Theme {
|
||||||
pub fn default(theme: &Theme) -> Style {
|
pub fn default(theme: &Theme) -> Style {
|
||||||
Style {
|
Style {
|
||||||
scale: 1.05,
|
scale: 1.05,
|
||||||
moved_item_overlay: Color::from(theme.cosmic().primary.base)
|
moved_item_overlay: Color::from(theme.cosmic().primary.base).scale_alpha(0.2),
|
||||||
.scale_alpha(0.2),
|
|
||||||
ghost_border: Border {
|
ghost_border: Border {
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
color: theme.cosmic().secondary.base.into(),
|
color: theme.cosmic().secondary.base.into(),
|
||||||
|
|
|
||||||
1325
src/ui/widgets/draggable/flex_row.rs
Normal file
|
|
@ -1,8 +1,10 @@
|
||||||
use cosmic::iced::Point;
|
use cosmic::iced::Point;
|
||||||
|
|
||||||
pub use self::column::column;
|
pub use self::column::column;
|
||||||
|
pub use self::flex_row::flex_row;
|
||||||
pub use self::row::row;
|
pub use self::row::row;
|
||||||
pub mod column;
|
pub mod column;
|
||||||
|
pub mod flex_row;
|
||||||
pub mod row;
|
pub mod row;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -27,19 +27,16 @@ use cosmic::iced::advanced::layout::{self, Layout};
|
||||||
use cosmic::iced::advanced::widget::{Operation, Tree, Widget, tree};
|
use cosmic::iced::advanced::widget::{Operation, Tree, Widget, tree};
|
||||||
use cosmic::iced::advanced::{Clipboard, Shell, overlay, renderer};
|
use cosmic::iced::advanced::{Clipboard, Shell, overlay, renderer};
|
||||||
use cosmic::iced::alignment::{self, Alignment};
|
use cosmic::iced::alignment::{self, Alignment};
|
||||||
use cosmic::iced::event::{self, Event};
|
use cosmic::iced::event::Event;
|
||||||
use cosmic::iced::{self, Transformation, mouse};
|
|
||||||
use cosmic::iced::{
|
use cosmic::iced::{
|
||||||
Background, Border, Color, Element, Length, Padding, Pixels,
|
self, Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle,
|
||||||
Point, Rectangle, Size, Vector,
|
Size, Transformation, Vector, mouse,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Action, DragEvent, DropPosition};
|
use super::{Action, DragEvent, DropPosition};
|
||||||
|
|
||||||
pub fn row<'a, Message, Theme, Renderer>(
|
pub fn row<'a, Message, Theme, Renderer>(
|
||||||
children: impl IntoIterator<
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Row<'a, Message, Theme, Renderer>
|
) -> Row<'a, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: renderer::Renderer,
|
Renderer: renderer::Renderer,
|
||||||
|
|
@ -108,9 +105,7 @@ where
|
||||||
|
|
||||||
/// Creates a [`Row`] with the given elements.
|
/// Creates a [`Row`] with the given elements.
|
||||||
pub fn with_children(
|
pub fn with_children(
|
||||||
children: impl IntoIterator<
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let iterator = children.into_iter();
|
let iterator = children.into_iter();
|
||||||
|
|
||||||
|
|
@ -125,9 +120,7 @@ where
|
||||||
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||||
/// call [`Row::width`] or [`Row::height`] accordingly.
|
/// call [`Row::width`] or [`Row::height`] accordingly.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn from_vec(
|
pub fn from_vec(children: Vec<Element<'a, Message, Theme, Renderer>>) -> Self {
|
||||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
spacing: 0.0,
|
spacing: 0.0,
|
||||||
padding: Padding::ZERO,
|
padding: Padding::ZERO,
|
||||||
|
|
@ -171,10 +164,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the vertical alignment of the contents of the [`Row`] .
|
/// Sets the vertical alignment of the contents of the [`Row`] .
|
||||||
pub fn align_y(
|
pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
|
||||||
mut self,
|
|
||||||
align: impl Into<alignment::Vertical>,
|
|
||||||
) -> Self {
|
|
||||||
self.align = Alignment::from(align.into());
|
self.align = Alignment::from(align.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -210,9 +200,7 @@ where
|
||||||
/// Adds an element to the [`Row`], if `Some`.
|
/// Adds an element to the [`Row`], if `Some`.
|
||||||
pub fn push_maybe(
|
pub fn push_maybe(
|
||||||
self,
|
self,
|
||||||
child: Option<
|
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||||
impl Into<Element<'a, Message, Theme, Renderer>>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
if let Some(child) = child {
|
if let Some(child) = child {
|
||||||
self.push(child)
|
self.push(child)
|
||||||
|
|
@ -223,10 +211,7 @@ where
|
||||||
|
|
||||||
/// Sets the style of the [`Row`].
|
/// Sets the style of the [`Row`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn style(
|
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||||
mut self,
|
|
||||||
style: impl Fn(&Theme) -> Style + 'a,
|
|
||||||
) -> Self
|
|
||||||
where
|
where
|
||||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||||
{
|
{
|
||||||
|
|
@ -236,10 +221,7 @@ where
|
||||||
|
|
||||||
/// Sets the style class of the [`Row`].
|
/// Sets the style class of the [`Row`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn class(
|
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||||
mut self,
|
|
||||||
class: impl Into<Theme::Class<'a>>,
|
|
||||||
) -> Self {
|
|
||||||
self.class = class.into();
|
self.class = class.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -247,9 +229,7 @@ where
|
||||||
/// Extends the [`Row`] with the given children.
|
/// Extends the [`Row`] with the given children.
|
||||||
pub fn extend(
|
pub fn extend(
|
||||||
self,
|
self,
|
||||||
children: impl IntoIterator<
|
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||||
Item = Element<'a, Message, Theme, Renderer>,
|
|
||||||
>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
children.into_iter().fold(self, Self::push)
|
children.into_iter().fold(self, Self::push)
|
||||||
}
|
}
|
||||||
|
|
@ -257,17 +237,12 @@ where
|
||||||
/// Turns the [`Row`] into a [`Wrapping`] row.
|
/// Turns the [`Row`] into a [`Wrapping`] row.
|
||||||
///
|
///
|
||||||
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
||||||
pub const fn wrap(
|
pub const fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
|
||||||
self,
|
|
||||||
) -> Wrapping<'a, Message, Theme, Renderer> {
|
|
||||||
Wrapping { row: self }
|
Wrapping { row: self }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The message produced by the [`Row`] when a child is dragged.
|
/// The message produced by the [`Row`] when a child is dragged.
|
||||||
pub fn on_drag(
|
pub fn on_drag(mut self, on_reorder: impl Fn(DragEvent) -> Message + 'a) -> Self {
|
||||||
mut self,
|
|
||||||
on_reorder: impl Fn(DragEvent) -> Message + 'a,
|
|
||||||
) -> Self {
|
|
||||||
self.on_drag = Some(Box::new(on_reorder));
|
self.on_drag = Some(Box::new(on_reorder));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -332,9 +307,7 @@ impl<'a, Message, Theme, Renderer: renderer::Renderer>
|
||||||
where
|
where
|
||||||
Theme: Catalog,
|
Theme: Catalog,
|
||||||
{
|
{
|
||||||
fn from_iter<
|
fn from_iter<T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>>(
|
||||||
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
|
||||||
>(
|
|
||||||
iter: T,
|
iter: T,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::with_children(iter)
|
Self::with_children(iter)
|
||||||
|
|
@ -371,7 +344,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
limits: &layout::Limits,
|
limits: &layout::Limits,
|
||||||
|
|
@ -385,69 +358,73 @@ where
|
||||||
self.padding,
|
self.padding,
|
||||||
self.spacing,
|
self.spacing,
|
||||||
self.align,
|
self.align,
|
||||||
&self.children,
|
&mut self.children,
|
||||||
&mut tree.children,
|
&mut tree.children,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn operate(
|
fn operate(
|
||||||
&self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
operation: &mut dyn Operation,
|
operation: &mut dyn Operation,
|
||||||
) {
|
) {
|
||||||
operation.container(
|
operation.container(None, layout.bounds());
|
||||||
None,
|
operation.traverse(&mut |operation| {
|
||||||
layout.bounds(),
|
self.children
|
||||||
&mut |operation| {
|
.iter_mut()
|
||||||
self.children
|
.zip(&mut tree.children)
|
||||||
.iter()
|
.zip(layout.children())
|
||||||
.zip(&mut tree.children)
|
.for_each(|((child, state), layout)| {
|
||||||
.zip(layout.children())
|
child
|
||||||
.for_each(|((child, state), layout)| {
|
.as_widget_mut()
|
||||||
child.as_widget().operate(
|
.operate(state, layout, renderer, operation);
|
||||||
state, layout, renderer, operation,
|
});
|
||||||
);
|
});
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
event: Event,
|
event: &Event,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
clipboard: &mut dyn Clipboard,
|
clipboard: &mut dyn Clipboard,
|
||||||
shell: &mut Shell<'_, Message>,
|
shell: &mut Shell<'_, Message>,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) -> event::Status {
|
) {
|
||||||
let mut event_status = event::Status::Ignored;
|
|
||||||
|
|
||||||
let action = tree.state.downcast_mut::<Action>();
|
let action = tree.state.downcast_mut::<Action>();
|
||||||
|
|
||||||
|
// let children have precedence
|
||||||
|
self.children
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&mut tree.children)
|
||||||
|
.zip(layout.children())
|
||||||
|
.for_each(|((child, state), layout)| {
|
||||||
|
child.as_widget_mut().update(
|
||||||
|
state,
|
||||||
|
&event.clone(),
|
||||||
|
layout,
|
||||||
|
cursor,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::Mouse(mouse::Event::ButtonPressed(
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||||
mouse::Button::Left,
|
if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
|
||||||
)) => {
|
for (index, child_layout) in layout.children().enumerate() {
|
||||||
if let Some(cursor_position) =
|
if child_layout.bounds().contains(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 {
|
*action = Action::Picking {
|
||||||
index,
|
index,
|
||||||
origin: cursor_position,
|
origin: cursor_position,
|
||||||
};
|
};
|
||||||
event_status = event::Status::Captured;
|
shell.capture_event();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -456,10 +433,8 @@ where
|
||||||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||||
match *action {
|
match *action {
|
||||||
Action::Picking { index, origin } => {
|
Action::Picking { index, origin } => {
|
||||||
if let Some(cursor_position) =
|
if let Some(cursor_position) = cursor.position()
|
||||||
cursor.position()
|
&& cursor_position.distance(origin) > self.deadband_zone
|
||||||
&& cursor_position.distance(origin)
|
|
||||||
> self.deadband_zone
|
|
||||||
{
|
{
|
||||||
// Start dragging
|
// Start dragging
|
||||||
*action = Action::Dragging {
|
*action = Action::Dragging {
|
||||||
|
|
@ -468,66 +443,44 @@ where
|
||||||
last_cursor: cursor_position,
|
last_cursor: cursor_position,
|
||||||
};
|
};
|
||||||
if let Some(on_reorder) = &self.on_drag {
|
if let Some(on_reorder) = &self.on_drag {
|
||||||
shell.publish(on_reorder(
|
shell.publish(on_reorder(DragEvent::Picked { index }));
|
||||||
DragEvent::Picked { index },
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
event_status = event::Status::Captured;
|
shell.capture_event();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Dragging { origin, index, .. } => {
|
Action::Dragging { origin, index, .. } => {
|
||||||
if let Some(cursor_position) =
|
if let Some(cursor_position) = cursor.position() {
|
||||||
cursor.position()
|
|
||||||
{
|
|
||||||
*action = Action::Dragging {
|
*action = Action::Dragging {
|
||||||
last_cursor: cursor_position,
|
last_cursor: cursor_position,
|
||||||
origin,
|
origin,
|
||||||
index,
|
index,
|
||||||
};
|
};
|
||||||
event_status = event::Status::Captured;
|
shell.capture_event();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Mouse(mouse::Event::ButtonReleased(
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||||
mouse::Button::Left,
|
|
||||||
)) => {
|
|
||||||
match *action {
|
match *action {
|
||||||
Action::Dragging { index, .. } => {
|
Action::Dragging { index, .. } => {
|
||||||
if let Some(cursor_position) =
|
if let Some(cursor_position) = cursor.position() {
|
||||||
cursor.position()
|
|
||||||
{
|
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
if bounds.contains(cursor_position) {
|
if bounds.contains(cursor_position) {
|
||||||
let (target_index, drop_position) =
|
let (target_index, drop_position) = self
|
||||||
self.compute_target_index(
|
.compute_target_index(cursor_position, layout, index);
|
||||||
cursor_position,
|
|
||||||
layout,
|
|
||||||
index,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(on_reorder) =
|
if let Some(on_reorder) = &self.on_drag {
|
||||||
&self.on_drag
|
shell.publish(on_reorder(DragEvent::Dropped {
|
||||||
{
|
index,
|
||||||
shell.publish(on_reorder(
|
target_index,
|
||||||
DragEvent::Dropped {
|
drop_position,
|
||||||
index,
|
}));
|
||||||
target_index,
|
shell.capture_event();
|
||||||
drop_position,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
event_status =
|
|
||||||
event::Status::Captured;
|
|
||||||
}
|
}
|
||||||
} else if let Some(on_reorder) =
|
} else if let Some(on_reorder) = &self.on_drag {
|
||||||
&self.on_drag
|
shell.publish(on_reorder(DragEvent::Canceled { index }));
|
||||||
{
|
shell.capture_event();
|
||||||
shell.publish(on_reorder(
|
|
||||||
DragEvent::Canceled { index },
|
|
||||||
));
|
|
||||||
event_status =
|
|
||||||
event::Status::Captured;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*action = Action::Idle;
|
*action = Action::Idle;
|
||||||
|
|
@ -541,27 +494,6 @@ where
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let child_status = self
|
|
||||||
.children
|
|
||||||
.iter_mut()
|
|
||||||
.zip(&mut tree.children)
|
|
||||||
.zip(layout.children())
|
|
||||||
.map(|((child, state), layout)| {
|
|
||||||
child.as_widget_mut().on_event(
|
|
||||||
state,
|
|
||||||
event.clone(),
|
|
||||||
layout,
|
|
||||||
cursor,
|
|
||||||
renderer,
|
|
||||||
clipboard,
|
|
||||||
shell,
|
|
||||||
viewport,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.fold(event::Status::Ignored, event::Status::merge);
|
|
||||||
|
|
||||||
event::Status::merge(event_status, child_status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
|
|
@ -583,9 +515,9 @@ where
|
||||||
.zip(&tree.children)
|
.zip(&tree.children)
|
||||||
.zip(layout.children())
|
.zip(layout.children())
|
||||||
.map(|((child, state), layout)| {
|
.map(|((child, state), layout)| {
|
||||||
child.as_widget().mouse_interaction(
|
child
|
||||||
state, layout, cursor, viewport, renderer,
|
.as_widget()
|
||||||
)
|
.mouse_interaction(state, layout, cursor, viewport, renderer)
|
||||||
})
|
})
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|
@ -615,20 +547,15 @@ where
|
||||||
|
|
||||||
// Determine the target index based on cursor position
|
// Determine the target index based on cursor position
|
||||||
let target_index = if cursor.position().is_some() {
|
let target_index = if cursor.position().is_some() {
|
||||||
let (target_index, _) = self
|
let (target_index, _) =
|
||||||
.compute_target_index(
|
self.compute_target_index(*last_cursor, layout, *index);
|
||||||
*last_cursor,
|
|
||||||
layout,
|
|
||||||
*index,
|
|
||||||
);
|
|
||||||
target_index.min(child_count - 1)
|
target_index.min(child_count - 1)
|
||||||
} else {
|
} else {
|
||||||
*index
|
*index
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store the width of the dragged item
|
// Store the width of the dragged item
|
||||||
let drag_bounds =
|
let drag_bounds = layout.children().nth(*index).unwrap().bounds();
|
||||||
layout.children().nth(*index).unwrap().bounds();
|
|
||||||
let drag_width = drag_bounds.width + self.spacing;
|
let drag_width = drag_bounds.width + self.spacing;
|
||||||
|
|
||||||
// Draw all children except the one being dragged
|
// Draw all children except the one being dragged
|
||||||
|
|
@ -636,118 +563,88 @@ where
|
||||||
for i in 0..child_count {
|
for i in 0..child_count {
|
||||||
let child = &self.children[i];
|
let child = &self.children[i];
|
||||||
let state = &tree.children[i];
|
let state = &tree.children[i];
|
||||||
let child_layout =
|
let child_layout = layout.children().nth(i).unwrap();
|
||||||
layout.children().nth(i).unwrap();
|
|
||||||
|
|
||||||
// Draw the dragged item separately
|
// Draw the dragged item separately
|
||||||
// TODO: Draw a shadow below the picked item to enhance the
|
// TODO: Draw a shadow below the picked item to enhance the
|
||||||
// floating effect
|
// floating effect
|
||||||
if i == *index {
|
if i == *index {
|
||||||
let scaling =
|
let scaling = Transformation::scale(style.scale);
|
||||||
Transformation::scale(style.scale);
|
let translation = *last_cursor - *origin * scaling;
|
||||||
let translation =
|
renderer.with_translation(translation, |renderer| {
|
||||||
*last_cursor - *origin * scaling;
|
renderer.with_transformation(scaling, |renderer| {
|
||||||
renderer.with_translation(
|
renderer.with_layer(child_layout.bounds(), |renderer| {
|
||||||
translation,
|
child.as_widget().draw(
|
||||||
|renderer| {
|
state,
|
||||||
renderer.with_transformation(
|
renderer,
|
||||||
scaling,
|
theme,
|
||||||
|renderer| {
|
defaults,
|
||||||
renderer.with_layer(
|
child_layout,
|
||||||
child_layout.bounds(),
|
cursor,
|
||||||
|renderer| {
|
viewport,
|
||||||
child
|
|
||||||
.as_widget()
|
|
||||||
.draw(
|
|
||||||
state,
|
|
||||||
renderer,
|
|
||||||
theme,
|
|
||||||
defaults,
|
|
||||||
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(
|
|
||||||
offset as f32 * drag_width,
|
|
||||||
0.0,
|
|
||||||
);
|
|
||||||
renderer.with_translation(
|
|
||||||
translation,
|
|
||||||
|renderer| {
|
|
||||||
child.as_widget().draw(
|
|
||||||
state,
|
|
||||||
renderer,
|
|
||||||
theme,
|
|
||||||
defaults,
|
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} 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,
|
||||||
|
};
|
||||||
|
|
||||||
// Keep track of the total translation so we can
|
let translation = Vector::new(offset as f32 * drag_width, 0.0);
|
||||||
// draw the "ghost" of the dragged item later
|
renderer.with_translation(translation, |renderer| {
|
||||||
translations -=
|
child.as_widget().draw(
|
||||||
(child_layout.bounds().width
|
state,
|
||||||
+ self.spacing)
|
renderer,
|
||||||
* offset.signum() as f32;
|
theme,
|
||||||
}
|
defaults,
|
||||||
},
|
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().width
|
||||||
|
+ self.spacing)
|
||||||
|
* offset.signum() as f32;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Draw a ghost of the dragged item in its would-be position
|
// Draw a ghost of the dragged item in its would-be position
|
||||||
let ghost_translation =
|
let ghost_translation = Vector::new(translations, 0.0);
|
||||||
Vector::new(translations, 0.0);
|
renderer.with_translation(ghost_translation, |renderer| {
|
||||||
renderer.with_translation(
|
renderer.fill_quad(
|
||||||
ghost_translation,
|
renderer::Quad {
|
||||||
|renderer| {
|
bounds: drag_bounds,
|
||||||
renderer.fill_quad(
|
border: style.ghost_border,
|
||||||
renderer::Quad {
|
..renderer::Quad::default()
|
||||||
bounds: drag_bounds,
|
},
|
||||||
border: style.ghost_border,
|
style.ghost_background,
|
||||||
..renderer::Quad::default()
|
);
|
||||||
},
|
});
|
||||||
style.ghost_background,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Draw all children normally when not dragging
|
// Draw all children normally when not dragging
|
||||||
|
|
@ -757,10 +654,9 @@ where
|
||||||
.zip(&tree.children)
|
.zip(&tree.children)
|
||||||
.zip(layout.children())
|
.zip(layout.children())
|
||||||
{
|
{
|
||||||
child.as_widget().draw(
|
child
|
||||||
state, renderer, theme, defaults, layout,
|
.as_widget()
|
||||||
cursor, viewport,
|
.draw(state, renderer, theme, defaults, layout, cursor, viewport);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -769,8 +665,9 @@ where
|
||||||
fn overlay<'b>(
|
fn overlay<'b>(
|
||||||
&'b mut self,
|
&'b mut self,
|
||||||
tree: &'b mut Tree,
|
tree: &'b mut Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'b>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
overlay::from_children(
|
overlay::from_children(
|
||||||
|
|
@ -778,13 +675,13 @@ where
|
||||||
tree,
|
tree,
|
||||||
layout,
|
layout,
|
||||||
renderer,
|
renderer,
|
||||||
|
viewport,
|
||||||
translation,
|
translation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer>
|
impl<'a, Message, Theme, Renderer> From<Row<'a, Message, Theme, Renderer>>
|
||||||
From<Row<'a, Message, Theme, Renderer>>
|
|
||||||
for Element<'a, Message, Theme, Renderer>
|
for Element<'a, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Message: 'a,
|
Message: 'a,
|
||||||
|
|
@ -803,12 +700,8 @@ where
|
||||||
///
|
///
|
||||||
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
||||||
#[allow(missing_debug_implementations)]
|
#[allow(missing_debug_implementations)]
|
||||||
pub struct Wrapping<
|
pub struct Wrapping<'a, Message, Theme = cosmic::Theme, Renderer = iced::Renderer>
|
||||||
'a,
|
where
|
||||||
Message,
|
|
||||||
Theme = cosmic::Theme,
|
|
||||||
Renderer = iced::Renderer,
|
|
||||||
> where
|
|
||||||
Theme: Catalog,
|
Theme: Catalog,
|
||||||
{
|
{
|
||||||
row: Row<'a, Message, Theme, Renderer>,
|
row: Row<'a, Message, Theme, Renderer>,
|
||||||
|
|
@ -833,7 +726,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
limits: &layout::Limits,
|
limits: &layout::Limits,
|
||||||
|
|
@ -859,34 +752,31 @@ where
|
||||||
Alignment::End => 1.0,
|
Alignment::End => 1.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let align =
|
let align = |row_start: std::ops::Range<usize>,
|
||||||
|row_start: std::ops::Range<usize>,
|
row_height: f32,
|
||||||
row_height: f32,
|
children: &mut Vec<layout::Node>| {
|
||||||
children: &mut Vec<layout::Node>| {
|
if align_factor != 0.0 {
|
||||||
if align_factor != 0.0 {
|
for node in &mut children[row_start] {
|
||||||
for node in &mut children[row_start] {
|
let height = node.size().height;
|
||||||
let height = node.size().height;
|
|
||||||
|
|
||||||
node.translate_mut(Vector::new(
|
node.translate_mut(Vector::new(
|
||||||
0.0,
|
0.0,
|
||||||
(row_height - height) / align_factor,
|
(row_height - height) / align_factor,
|
||||||
));
|
));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (i, child) in self.row.children.iter().enumerate() {
|
for (i, child) in self.row.children.iter_mut().enumerate() {
|
||||||
let node = child.as_widget().layout(
|
let node =
|
||||||
&mut tree.children[i],
|
child
|
||||||
renderer,
|
.as_widget_mut()
|
||||||
&limits,
|
.layout(&mut tree.children[i], renderer, &limits);
|
||||||
);
|
|
||||||
|
|
||||||
let child_size = node.size();
|
let child_size = node.size();
|
||||||
|
|
||||||
if x != 0.0 && x + child_size.width > max_width {
|
if x != 0.0 && x + child_size.width > max_width {
|
||||||
intrinsic_size.width =
|
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
|
||||||
intrinsic_size.width.max(x - spacing);
|
|
||||||
|
|
||||||
align(row_start..i, row_height, &mut children);
|
align(row_start..i, row_height, &mut children);
|
||||||
|
|
||||||
|
|
@ -898,36 +788,27 @@ where
|
||||||
|
|
||||||
row_height = row_height.max(child_size.height);
|
row_height = row_height.max(child_size.height);
|
||||||
|
|
||||||
children.push(node.move_to((
|
children.push(
|
||||||
x + self.row.padding.left,
|
node.move_to((x + self.row.padding.left, y + self.row.padding.top)),
|
||||||
y + self.row.padding.top,
|
);
|
||||||
)));
|
|
||||||
|
|
||||||
x += child_size.width + spacing;
|
x += child_size.width + spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if x != 0.0 {
|
if x != 0.0 {
|
||||||
intrinsic_size.width =
|
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
|
||||||
intrinsic_size.width.max(x - spacing);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
intrinsic_size.height = y + row_height;
|
intrinsic_size.height = y + row_height;
|
||||||
align(row_start..children.len(), row_height, &mut children);
|
align(row_start..children.len(), row_height, &mut children);
|
||||||
|
|
||||||
let size = limits.resolve(
|
let size = limits.resolve(self.row.width, self.row.height, intrinsic_size);
|
||||||
self.row.width,
|
|
||||||
self.row.height,
|
|
||||||
intrinsic_size,
|
|
||||||
);
|
|
||||||
|
|
||||||
layout::Node::with_children(
|
layout::Node::with_children(size.expand(self.row.padding), children)
|
||||||
size.expand(self.row.padding),
|
|
||||||
children,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn operate(
|
fn operate(
|
||||||
&self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
|
|
@ -936,20 +817,19 @@ where
|
||||||
self.row.operate(tree, layout, renderer, operation);
|
self.row.operate(tree, layout, renderer, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut Tree,
|
tree: &mut Tree,
|
||||||
event: Event,
|
event: &Event,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
clipboard: &mut dyn Clipboard,
|
clipboard: &mut dyn Clipboard,
|
||||||
shell: &mut Shell<'_, Message>,
|
shell: &mut Shell<'_, Message>,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) -> event::Status {
|
) {
|
||||||
self.row.on_event(
|
self.row.update(
|
||||||
tree, event, layout, cursor, renderer, clipboard, shell,
|
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||||
viewport,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -961,9 +841,8 @@ where
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
) -> mouse::Interaction {
|
) -> mouse::Interaction {
|
||||||
self.row.mouse_interaction(
|
self.row
|
||||||
tree, layout, cursor, viewport, renderer,
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
|
|
@ -976,24 +855,24 @@ where
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
self.row.draw(
|
self.row
|
||||||
tree, renderer, theme, style, layout, cursor, viewport,
|
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overlay<'b>(
|
fn overlay<'b>(
|
||||||
&'b mut self,
|
&'b mut self,
|
||||||
tree: &'b mut Tree,
|
tree: &'b mut Tree,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'b>,
|
||||||
renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
self.row.overlay(tree, layout, renderer, translation)
|
self.row
|
||||||
|
.overlay(tree, layout, renderer, viewport, translation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer>
|
impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
|
||||||
From<Wrapping<'a, Message, Theme, Renderer>>
|
|
||||||
for Element<'a, Message, Theme, Renderer>
|
for Element<'a, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Message: 'a,
|
Message: 'a,
|
||||||
|
|
@ -1049,19 +928,15 @@ impl Catalog for cosmic::Theme {
|
||||||
pub fn default(theme: &cosmic::Theme) -> Style {
|
pub fn default(theme: &cosmic::Theme) -> Style {
|
||||||
Style {
|
Style {
|
||||||
scale: 1.05,
|
scale: 1.05,
|
||||||
moved_item_overlay: Color::from(
|
moved_item_overlay: Color::from(theme.cosmic().primary.base.color)
|
||||||
theme.cosmic().primary.base.color,
|
.scale_alpha(0.2),
|
||||||
)
|
|
||||||
.scale_alpha(0.2),
|
|
||||||
ghost_border: Border {
|
ghost_border: Border {
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
color: theme.cosmic().secondary.base.color.into(),
|
color: theme.cosmic().secondary.base.color.into(),
|
||||||
radius: 0.0.into(),
|
radius: 0.0.into(),
|
||||||
},
|
},
|
||||||
ghost_background: Color::from(
|
ghost_background: Color::from(theme.cosmic().secondary.base.color)
|
||||||
theme.cosmic().secondary.base.color,
|
.scale_alpha(0.2)
|
||||||
)
|
.into(),
|
||||||
.scale_alpha(0.2)
|
|
||||||
.into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
239
src/ui/widgets/loaded_image.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
use cosmic::iced::{core as iced_core, widget as iced_widget};
|
||||||
|
use iced_core::event::Event;
|
||||||
|
use iced_core::widget::{Operation, Tree};
|
||||||
|
use iced_core::{
|
||||||
|
Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse,
|
||||||
|
overlay, renderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn loaded_image<'a, Message: 'static, Theme, Renderer>(
|
||||||
|
handle: impl Into<<cosmic::Renderer as iced_core::image::Renderer>::Handle>,
|
||||||
|
content: impl Into<cosmic::iced::Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> LoadedImage<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Theme: iced_widget::container::Catalog,
|
||||||
|
<Theme as iced_widget::container::Catalog>::Class<'a>:
|
||||||
|
From<cosmic::theme::Container<'a>>,
|
||||||
|
Renderer: iced_core::Renderer
|
||||||
|
+ iced_core::image::Renderer<Handle = cosmic::widget::image::Handle>,
|
||||||
|
<Renderer as iced_core::image::Renderer>::Handle: 'a,
|
||||||
|
{
|
||||||
|
LoadedImage::new(handle.into(), content.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forces the wrapped image to be loaded before drawing.
|
||||||
|
///
|
||||||
|
/// May cause a dropped frame if the image is not already in the cache.
|
||||||
|
/// This is useful when you want to ensure an image is loaded before it is drawn, for example when swapping out a placeholder.
|
||||||
|
/// Otherwise, the image may be blank until the next redraw.
|
||||||
|
#[allow(missing_debug_implementations)]
|
||||||
|
pub struct LoadedImage<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_core::Renderer + iced_core::image::Renderer,
|
||||||
|
{
|
||||||
|
handle: <Renderer as iced_core::image::Renderer>::Handle,
|
||||||
|
content: cosmic::iced::Element<'a, Message, Theme, Renderer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> LoadedImage<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_core::Renderer + iced_core::image::Renderer,
|
||||||
|
<Renderer as iced_core::image::Renderer>::Handle: 'a,
|
||||||
|
{
|
||||||
|
/// Creates an empty [`LoadedImage`].
|
||||||
|
pub(crate) fn new(
|
||||||
|
handle: <Renderer as iced_core::image::Renderer>::Handle,
|
||||||
|
content: impl Into<cosmic::iced::Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> Self {
|
||||||
|
LoadedImage {
|
||||||
|
handle,
|
||||||
|
content: content.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for LoadedImage<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_core::Renderer + iced_core::image::Renderer,
|
||||||
|
{
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![Tree::new(&self.content)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&mut self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(std::slice::from_mut(&mut self.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> iced_core::Size<Length> {
|
||||||
|
self.content.as_widget().size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let node =
|
||||||
|
self.content
|
||||||
|
.as_widget_mut()
|
||||||
|
.layout(&mut tree.children[0], renderer, limits);
|
||||||
|
let size = node.size();
|
||||||
|
layout::Node::with_children(size, vec![node])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation,
|
||||||
|
) {
|
||||||
|
operation.container(None, layout.bounds());
|
||||||
|
operation.traverse(&mut |operation| {
|
||||||
|
self.content.as_widget_mut().operate(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout
|
||||||
|
.children()
|
||||||
|
.next()
|
||||||
|
.expect("There should always be a child")
|
||||||
|
.with_virtual_offset(layout.virtual_offset()),
|
||||||
|
renderer,
|
||||||
|
operation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
renderer: &Renderer,
|
||||||
|
clipboard: &mut dyn Clipboard,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
&mut tree.children[0],
|
||||||
|
event,
|
||||||
|
layout
|
||||||
|
.children()
|
||||||
|
.next()
|
||||||
|
.expect("There should always be a child")
|
||||||
|
.with_virtual_offset(layout.virtual_offset()),
|
||||||
|
cursor_position,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
shell,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
let content_layout = layout
|
||||||
|
.children()
|
||||||
|
.next()
|
||||||
|
.expect("There should always be a child");
|
||||||
|
|
||||||
|
self.content.as_widget().mouse_interaction(
|
||||||
|
&tree.children[0],
|
||||||
|
content_layout.with_virtual_offset(layout.virtual_offset()),
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &Tree,
|
||||||
|
renderer: &mut Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
renderer_style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor_position: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
let content_layout = layout
|
||||||
|
.children()
|
||||||
|
.next()
|
||||||
|
.expect("There should always be a child");
|
||||||
|
|
||||||
|
// forces image to be loaded before drawing
|
||||||
|
_ = renderer.load_image(&self.handle);
|
||||||
|
self.content.as_widget().draw(
|
||||||
|
&tree.children[0],
|
||||||
|
renderer,
|
||||||
|
theme,
|
||||||
|
renderer_style,
|
||||||
|
content_layout.with_virtual_offset(layout.virtual_offset()),
|
||||||
|
cursor_position,
|
||||||
|
viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.content.as_widget_mut().overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout
|
||||||
|
.children()
|
||||||
|
.next()
|
||||||
|
.expect("There should always be a child")
|
||||||
|
.with_virtual_offset(layout.virtual_offset()),
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drag_destinations(
|
||||||
|
&self,
|
||||||
|
state: &Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
|
||||||
|
) {
|
||||||
|
let content_layout = layout
|
||||||
|
.children()
|
||||||
|
.next()
|
||||||
|
.expect("There should always be a child");
|
||||||
|
self.content.as_widget().drag_destinations(
|
||||||
|
&state.children[0],
|
||||||
|
content_layout.with_virtual_offset(layout.virtual_offset()),
|
||||||
|
renderer,
|
||||||
|
dnd_rectangles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::use_self)]
|
||||||
|
impl<'a, Message, Theme, Renderer> From<LoadedImage<'a, Message, Theme, Renderer>>
|
||||||
|
for Element<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Message: 'a,
|
||||||
|
Renderer: 'a + iced_core::Renderer + iced_core::image::Renderer,
|
||||||
|
Theme: 'a,
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
c: LoadedImage<'a, Message, Theme, Renderer>,
|
||||||
|
) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
Self::new(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,5 +2,6 @@
|
||||||
#[allow(clippy::nursery)]
|
#[allow(clippy::nursery)]
|
||||||
#[allow(clippy::pedantic)]
|
#[allow(clippy::pedantic)]
|
||||||
pub mod draggable;
|
pub mod draggable;
|
||||||
pub mod slide_text;
|
pub mod loaded_image;
|
||||||
pub mod verse_editor;
|
pub mod verse_editor;
|
||||||
|
// pub mod slide_text;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ use cosmic::iced::advanced::renderer;
|
||||||
use cosmic::iced::advanced::widget::{self, Widget};
|
use cosmic::iced::advanced::widget::{self, Widget};
|
||||||
use cosmic::iced::border;
|
use cosmic::iced::border;
|
||||||
use cosmic::iced::mouse;
|
use cosmic::iced::mouse;
|
||||||
|
use cosmic::iced::wgpu::Primitive;
|
||||||
|
use cosmic::iced::wgpu::primitive::Renderer as PrimitiveRenderer;
|
||||||
use cosmic::iced::{Color, Element, Length, Rectangle, Size};
|
use cosmic::iced::{Color, Element, Length, Rectangle, Size};
|
||||||
use cosmic::iced_wgpu::Primitive;
|
|
||||||
use cosmic::iced_wgpu::primitive::Renderer as PrimitiveRenderer;
|
|
||||||
|
|
||||||
pub struct SlideText {
|
pub struct SlideText {
|
||||||
_text: String,
|
_text: String,
|
||||||
|
|
@ -40,7 +40,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&self,
|
&mut self,
|
||||||
_tree: &mut widget::Tree,
|
_tree: &mut widget::Tree,
|
||||||
_renderer: &Renderer,
|
_renderer: &Renderer,
|
||||||
_limits: &layout::Limits,
|
_limits: &layout::Limits,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
use cosmic::{
|
use cosmic::cosmic_theme::palette::WithAlpha;
|
||||||
Element, Task,
|
use cosmic::iced::widget::{column, row};
|
||||||
cosmic_theme::palette::WithAlpha,
|
use cosmic::iced::{Background, Border};
|
||||||
iced::{Background, Border},
|
use cosmic::widget::{button, combo_box, container, icon, space, text_editor};
|
||||||
iced_widget::{column, row},
|
use cosmic::{Element, Task, theme};
|
||||||
theme,
|
|
||||||
widget::{
|
|
||||||
button, combo_box, container, horizontal_space, icon,
|
|
||||||
text_editor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::core::songs::VerseName;
|
use crate::core::songs::VerseName;
|
||||||
|
|
||||||
|
|
@ -39,6 +33,7 @@ pub enum Action {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
impl VerseEditor {
|
impl VerseEditor {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(verse: VerseName, lyric: &str) -> Self {
|
pub fn new(verse: VerseName, lyric: &str) -> Self {
|
||||||
|
|
@ -47,9 +42,7 @@ impl VerseEditor {
|
||||||
lyric: lyric.to_string(),
|
lyric: lyric.to_string(),
|
||||||
content: text_editor::Content::with_text(lyric),
|
content: text_editor::Content::with_text(lyric),
|
||||||
editing_verse_name: false,
|
editing_verse_name: false,
|
||||||
verse_name_combo: combo_box::State::new(
|
verse_name_combo: combo_box::State::new(VerseName::all_names()),
|
||||||
VerseName::all_names(),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn update(&mut self, message: Message) -> Action {
|
pub fn update(&mut self, message: Message) -> Action {
|
||||||
|
|
@ -62,12 +55,12 @@ impl VerseEditor {
|
||||||
let verse = self.verse_name;
|
let verse = self.verse_name;
|
||||||
Action::UpdateVerse((verse, lyrics))
|
Action::UpdateVerse((verse, lyrics))
|
||||||
}
|
}
|
||||||
text_editor::Action::Scroll { pixels } => {
|
text_editor::Action::Scroll { lines } => {
|
||||||
if self.content.line_count() > 6 {
|
if self.content.line_count() > 6 {
|
||||||
self.content.perform(action);
|
self.content.perform(action);
|
||||||
Action::None
|
Action::None
|
||||||
} else {
|
} else {
|
||||||
Action::ScrollVerses(pixels)
|
Action::ScrollVerses(lines as f32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -75,9 +68,7 @@ impl VerseEditor {
|
||||||
Action::None
|
Action::None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Message::UpdateVerseName(verse_name) => {
|
Message::UpdateVerseName(verse_name) => Action::UpdateVerseName(verse_name),
|
||||||
Action::UpdateVerseName(verse_name)
|
|
||||||
}
|
|
||||||
Message::EditVerseName => {
|
Message::EditVerseName => {
|
||||||
self.editing_verse_name = !self.editing_verse_name;
|
self.editing_verse_name = !self.editing_verse_name;
|
||||||
Action::None
|
Action::None
|
||||||
|
|
@ -96,9 +87,7 @@ impl VerseEditor {
|
||||||
} = theme::spacing();
|
} = theme::spacing();
|
||||||
|
|
||||||
let delete_button = button::text("Delete")
|
let delete_button = button::text("Delete")
|
||||||
.trailing_icon(
|
.trailing_icon(icon::from_name("window-close-symbolic"))
|
||||||
icon::from_name("view-close").symbolic(true),
|
|
||||||
)
|
|
||||||
.class(theme::Button::Destructive)
|
.class(theme::Button::Destructive)
|
||||||
.on_press(Message::DeleteVerse(self.verse_name));
|
.on_press(Message::DeleteVerse(self.verse_name));
|
||||||
let combo = combo_box(
|
let combo = combo_box(
|
||||||
|
|
@ -108,61 +97,43 @@ impl VerseEditor {
|
||||||
Message::UpdateVerseName,
|
Message::UpdateVerseName,
|
||||||
);
|
);
|
||||||
|
|
||||||
let verse_title =
|
let verse_title = row![combo, space::horizontal(), delete_button];
|
||||||
row![combo, horizontal_space(), delete_button];
|
|
||||||
|
|
||||||
let lyric: Element<Message> = if self.verse_name
|
let lyric: Element<Message> = if self.verse_name == VerseName::Blank {
|
||||||
== VerseName::Blank
|
space::horizontal().into()
|
||||||
{
|
|
||||||
horizontal_space().into()
|
|
||||||
} else {
|
} else {
|
||||||
text_editor(&self.content)
|
text_editor(&self.content)
|
||||||
.on_action(Message::UpdateLyric)
|
.on_action(Message::UpdateLyric)
|
||||||
.padding(space_m)
|
.padding(space_m)
|
||||||
.class(theme::iced::TextEditor::Custom(Box::new(
|
.class(theme::iced::TextEditor::Custom(Box::new(move |t, s| {
|
||||||
move |t, s| {
|
let neutral = t.cosmic().palette.neutral_9;
|
||||||
let neutral = t.cosmic().palette.neutral_9;
|
let mut base_style = text_editor::Style {
|
||||||
let mut base_style = text_editor::Style {
|
background: Background::Color(
|
||||||
background: Background::Color(
|
t.cosmic().background.small_widget.with_alpha(0.25).into(),
|
||||||
t.cosmic()
|
),
|
||||||
.background
|
border: Border::default()
|
||||||
.small_widget
|
.rounded(space_s as u8)
|
||||||
.with_alpha(0.25)
|
.width(2)
|
||||||
.into(),
|
.color(t.cosmic().bg_component_divider()),
|
||||||
),
|
placeholder: neutral.with_alpha(0.7).into(),
|
||||||
border: Border::default()
|
value: neutral.into(),
|
||||||
.rounded(space_s)
|
selection: t.cosmic().accent.base.into(),
|
||||||
.width(2)
|
};
|
||||||
.color(
|
let hovered_border = Border::default()
|
||||||
t.cosmic().bg_component_divider(),
|
.rounded(space_s as u8)
|
||||||
),
|
.width(3)
|
||||||
icon: t
|
.color(t.cosmic().accent.hover);
|
||||||
.cosmic()
|
match s {
|
||||||
.primary_component_color()
|
text_editor::Status::Hovered
|
||||||
.into(),
|
| text_editor::Status::Focused { .. } => {
|
||||||
placeholder: neutral
|
base_style.border = hovered_border;
|
||||||
.with_alpha(0.7)
|
base_style
|
||||||
.into(),
|
|
||||||
value: neutral.into(),
|
|
||||||
selection: t.cosmic().accent.base.into(),
|
|
||||||
};
|
|
||||||
let hovered_border = Border::default()
|
|
||||||
.rounded(space_s)
|
|
||||||
.width(3)
|
|
||||||
.color(t.cosmic().accent.hover);
|
|
||||||
match s {
|
|
||||||
text_editor::Status::Active => base_style,
|
|
||||||
text_editor::Status::Hovered
|
|
||||||
| text_editor::Status::Focused => {
|
|
||||||
base_style.border = hovered_border;
|
|
||||||
base_style
|
|
||||||
}
|
|
||||||
text_editor::Status::Disabled => {
|
|
||||||
base_style
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
text_editor::Status::Active | text_editor::Status::Disabled => {
|
||||||
)))
|
base_style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})))
|
||||||
.height(150)
|
.height(150)
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
BIN
test.db
77
xyz.cochrun.lumina.yml
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
app-id: xyz.cochrun.lumina
|
||||||
|
runtime: org.freedesktop.Platform
|
||||||
|
runtime-version: '25.08'
|
||||||
|
sdk: org.freedesktop.Sdk
|
||||||
|
sdk-extensions:
|
||||||
|
- org.freedesktop.Sdk.Extension.rust-nightly
|
||||||
|
- org.freedesktop.Sdk.Extension.llvm22
|
||||||
|
base: com.system76.Cosmic.BaseApp
|
||||||
|
command: lumina
|
||||||
|
add-extensions:
|
||||||
|
org.freedesktop.Platform.ffmpeg-full:
|
||||||
|
version: '25.08' # replace by appropriate version
|
||||||
|
directory: lib/ffmpeg
|
||||||
|
add-ld-path: .
|
||||||
|
org.freedesktop.Platform.GStreamer:
|
||||||
|
version: '25.08'
|
||||||
|
add-ld-path: .
|
||||||
|
finish-args:
|
||||||
|
- --share=ipc
|
||||||
|
- --socket=fallback-x11
|
||||||
|
- --socket=wayland
|
||||||
|
- --device=dri
|
||||||
|
- --share=network
|
||||||
|
- --talk-name=com.system76.CosmicSettingsDaemon
|
||||||
|
- --talk-name=com.system76.CosmicSettingsDaemon.*
|
||||||
|
- --filesystem=home:rw
|
||||||
|
- --filesystem=xdg-config/cosmic:rw
|
||||||
|
- --filesystem=xdg-data/lumina:rw
|
||||||
|
- --filesystem=xdg-cache/lumina:rw
|
||||||
|
build-options:
|
||||||
|
append-path: /usr/lib/sdk/rust-nightly/bin:/usr/lib/sdk/llvm22/bin
|
||||||
|
# append-path: /usr/lib/sdk/llvm22/bin
|
||||||
|
prepend-ld-library-path: /usr/lib/sdk/llvm22/lib
|
||||||
|
env:
|
||||||
|
CARGO_HOME: /run/build/lumina/cargo
|
||||||
|
LIBCLANG_PATH: /usr/lib/sdk/llvm22/lib
|
||||||
|
BINDGEN_EXTRA_CLANG_ARGS: -I/usr/lib/sdk/llvm22/lib/clang/22/include
|
||||||
|
PKG_CONFIG_PATH: /app/lib64/pkgconfig:/app/lib/pkgconfig:/app/share/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig
|
||||||
|
# CC: gcc
|
||||||
|
# CXX: g++
|
||||||
|
# CARGO_NET_OFFLINE: 'true'
|
||||||
|
modules:
|
||||||
|
# - name: mupdf-rs
|
||||||
|
# buildsystem: simple
|
||||||
|
# build-commands:
|
||||||
|
# - cargo build --offline --release
|
||||||
|
# sources:
|
||||||
|
# - type: git
|
||||||
|
# url: https://github.com/messense/mupdf-rs.git
|
||||||
|
# commit: 902d44bf7becfc84f8349c524cb8acfb18a6f3d4
|
||||||
|
# - mupdf-cargo-sources.json
|
||||||
|
|
||||||
|
- name: lumina
|
||||||
|
buildsystem: simple
|
||||||
|
build-options:
|
||||||
|
env:
|
||||||
|
CARGO_HOME: /run/build/lumina/cargo
|
||||||
|
DATABASE_URL: sqlite://./test.db
|
||||||
|
# CARGO_NET_OFFLINE: 'true'
|
||||||
|
build-commands:
|
||||||
|
- just build-offline
|
||||||
|
# - cargo build --release
|
||||||
|
|
||||||
|
- install -Dm755 target/release/lumina -t /app/bin
|
||||||
|
- install -Dm644 res/icons/lumina.svg -t /app/share/icons/hicolor/scalable/apps
|
||||||
|
- install -Dm644 res/${FLATPAK_ID}.desktop -t /app/share/applications
|
||||||
|
- install -Dm644 res/${FLATPAK_ID}.metainfo.xml -t /app/share/metainfo
|
||||||
|
|
||||||
|
sources:
|
||||||
|
# - type: git
|
||||||
|
# url: https://git.tfcconnection.org/chris/lumina.git
|
||||||
|
# commit: 53d1ad6163634658a274293d3a2ca7a69a4d9421
|
||||||
|
|
||||||
|
- type: dir
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- cargo-sources.json
|
||||||