[formatting]
This commit is contained in:
parent
42c3bd3068
commit
44749e154f
29 changed files with 2548 additions and 4412 deletions
|
|
@ -1 +1 @@
|
|||
excessive-nesting-threshold = 5
|
||||
excessive-nesting-threshold = 7
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
max_width = 70
|
||||
max_width = 90
|
||||
style_edition = "2024"
|
||||
# version = "Two"
|
||||
imports_granularity = "Module"
|
||||
115
src/core/file.rs
115
src/core/file.rs
|
|
@ -24,8 +24,7 @@ pub fn save(
|
|||
}
|
||||
let save_file = File::create(path).into_diagnostic()?;
|
||||
let ron_pretty = ron::ser::PrettyConfig::default();
|
||||
let ron = ron::ser::to_string_pretty(&list, ron_pretty)
|
||||
.into_diagnostic()?;
|
||||
let ron = ron::ser::to_string_pretty(&list, ron_pretty).into_diagnostic()?;
|
||||
|
||||
let encoder = Encoder::new(save_file, 3)
|
||||
.expect("file encoder shouldn't fail")
|
||||
|
|
@ -35,8 +34,7 @@ pub fn save(
|
|||
"there should be a data directory, ~/.local/share/ for linux, but couldn't find it",
|
||||
);
|
||||
temp_dir.push("lumina");
|
||||
let mut s: String =
|
||||
iter::repeat_with(fastrand::alphanumeric).take(5).collect();
|
||||
let mut s: String = iter::repeat_with(fastrand::alphanumeric).take(5).collect();
|
||||
s.insert_str(0, "temp_");
|
||||
temp_dir.push(s);
|
||||
fs::create_dir_all(&temp_dir).into_diagnostic()?;
|
||||
|
|
@ -60,9 +58,7 @@ pub fn save(
|
|||
}
|
||||
match tar.append_file("serviceitems.ron", &mut f) {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
"should have added serviceitems.ron to the file"
|
||||
);
|
||||
debug!("should have added serviceitems.ron to the file");
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e);
|
||||
|
|
@ -92,23 +88,18 @@ pub fn save(
|
|||
audio = song.audio.clone();
|
||||
}
|
||||
ServiceItemKind::Image(image) => {
|
||||
background = Some(
|
||||
Background::try_from(image.path.clone())
|
||||
.into_diagnostic()?,
|
||||
);
|
||||
background =
|
||||
Some(Background::try_from(image.path.clone()).into_diagnostic()?);
|
||||
audio = None;
|
||||
}
|
||||
ServiceItemKind::Video(video) => {
|
||||
background = Some(
|
||||
Background::try_from(video.path.clone())
|
||||
.into_diagnostic()?,
|
||||
);
|
||||
background =
|
||||
Some(Background::try_from(video.path.clone()).into_diagnostic()?);
|
||||
audio = None;
|
||||
}
|
||||
ServiceItemKind::Presentation(presentation) => {
|
||||
background = Some(
|
||||
Background::try_from(presentation.path.clone())
|
||||
.into_diagnostic()?,
|
||||
Background::try_from(presentation.path.clone()).into_diagnostic()?,
|
||||
);
|
||||
audio = None;
|
||||
}
|
||||
|
|
@ -151,12 +142,10 @@ pub fn save(
|
|||
#[allow(clippy::too_many_lines)]
|
||||
pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
||||
let decoder =
|
||||
Decoder::new(fs::File::open(&path).into_diagnostic()?)
|
||||
.into_diagnostic()?;
|
||||
Decoder::new(fs::File::open(&path).into_diagnostic()?).into_diagnostic()?;
|
||||
let mut tar = Archive::new(decoder);
|
||||
|
||||
let mut cache_dir =
|
||||
dirs::cache_dir().expect("Should be a cache dir");
|
||||
let mut cache_dir = dirs::cache_dir().expect("Should be a cache dir");
|
||||
cache_dir.push("lumina");
|
||||
cache_dir.push("cached_save_files");
|
||||
|
||||
|
|
@ -173,8 +162,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
.to_os_string()
|
||||
.into_string()
|
||||
.expect("Should be fine");
|
||||
let save_name = save_name_string
|
||||
.trim_end_matches(&format!(".{save_name_ext}"));
|
||||
let save_name = save_name_string.trim_end_matches(&format!(".{save_name_ext}"));
|
||||
cache_dir.push(save_name);
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(&cache_dir) {
|
||||
|
|
@ -190,9 +178,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
let mut dir = fs::read_dir(&cache_dir).into_diagnostic()?;
|
||||
let ron_file = dir
|
||||
.find_map(|file| {
|
||||
if file.as_ref().ok()?.path().extension()?.to_str()?
|
||||
== "ron"
|
||||
{
|
||||
if file.as_ref().ok()?.path().extension()?.to_str()? == "ron" {
|
||||
Some(file.ok()?.path())
|
||||
} else {
|
||||
None
|
||||
|
|
@ -200,12 +186,10 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
})
|
||||
.expect("Should have a ron file");
|
||||
|
||||
let ron_string =
|
||||
fs::read_to_string(ron_file).into_diagnostic()?;
|
||||
let ron_string = fs::read_to_string(ron_file).into_diagnostic()?;
|
||||
|
||||
let mut items =
|
||||
ron::de::from_str::<Vec<ServiceItem>>(&ron_string)
|
||||
.into_diagnostic()?;
|
||||
ron::de::from_str::<Vec<ServiceItem>>(&ron_string).into_diagnostic()?;
|
||||
|
||||
for item in &mut items {
|
||||
let dir = fs::read_dir(&cache_dir).into_diagnostic()?;
|
||||
|
|
@ -213,33 +197,20 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
for slide in &mut item.slides {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
let audio_path =
|
||||
slide.audio().clone().unwrap_or_default();
|
||||
let text_path = slide
|
||||
.text_svg
|
||||
.as_ref()
|
||||
.and_then(|svg| svg.path.clone());
|
||||
if Some(file_name.as_os_str())
|
||||
== slide.background.path.file_name()
|
||||
{
|
||||
let audio_path = slide.audio().clone().unwrap_or_default();
|
||||
let text_path =
|
||||
slide.text_svg.as_ref().and_then(|svg| svg.path.clone());
|
||||
if Some(file_name.as_os_str()) == slide.background.path.file_name() {
|
||||
slide.background.path = file.path();
|
||||
} else if Some(file_name.as_os_str())
|
||||
== audio_path.file_name()
|
||||
{
|
||||
let new_slide = slide
|
||||
.clone()
|
||||
.set_audio(Some(file.path()));
|
||||
} else if Some(file_name.as_os_str()) == audio_path.file_name() {
|
||||
let new_slide = slide.clone().set_audio(Some(file.path()));
|
||||
*slide = new_slide;
|
||||
} else if Some(file_name.as_os_str())
|
||||
== text_path
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.file_name()
|
||||
== text_path.clone().unwrap_or_default().file_name()
|
||||
&& let Some(svg) = slide.text_svg.as_mut()
|
||||
{
|
||||
svg.path = Some(file.path());
|
||||
svg.handle =
|
||||
Some(Handle::from_path(file.path()));
|
||||
svg.handle = Some(Handle::from_path(file.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -248,8 +219,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
ServiceItemKind::Song(song) => {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
let audio_path =
|
||||
song.audio.clone().unwrap_or_default();
|
||||
let audio_path = song.audio.clone().unwrap_or_default();
|
||||
if Some(file_name.as_os_str())
|
||||
== song
|
||||
.background
|
||||
|
|
@ -259,14 +229,11 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
.file_name()
|
||||
{
|
||||
let background = song.background.clone();
|
||||
song.background =
|
||||
background.map(|mut background| {
|
||||
background.path = file.path();
|
||||
background
|
||||
});
|
||||
} else if Some(file_name.as_os_str())
|
||||
== audio_path.file_name()
|
||||
{
|
||||
song.background = background.map(|mut background| {
|
||||
background.path = file.path();
|
||||
background
|
||||
});
|
||||
} else if Some(file_name.as_os_str()) == audio_path.file_name() {
|
||||
song.audio = Some(file.path());
|
||||
}
|
||||
}
|
||||
|
|
@ -274,9 +241,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
ServiceItemKind::Video(video) => {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
if Some(file_name.as_os_str())
|
||||
== video.path.file_name()
|
||||
{
|
||||
if Some(file_name.as_os_str()) == video.path.file_name() {
|
||||
video.path = file.path();
|
||||
}
|
||||
}
|
||||
|
|
@ -284,9 +249,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
ServiceItemKind::Image(image) => {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
if Some(file_name.as_os_str())
|
||||
== image.path.file_name()
|
||||
{
|
||||
if Some(file_name.as_os_str()) == image.path.file_name() {
|
||||
image.path = file.path();
|
||||
}
|
||||
}
|
||||
|
|
@ -294,9 +257,7 @@ pub fn load(path: impl AsRef<Path>) -> Result<Vec<ServiceItem>> {
|
|||
ServiceItemKind::Presentation(presentation) => {
|
||||
if let Ok(file) = file.as_ref() {
|
||||
let file_name = file.file_name();
|
||||
if Some(file_name.as_os_str())
|
||||
== presentation.path.file_name()
|
||||
{
|
||||
if Some(file_name.as_os_str()) == presentation.path.file_name() {
|
||||
presentation.path = file.path();
|
||||
}
|
||||
}
|
||||
|
|
@ -361,10 +322,9 @@ mod test {
|
|||
.expect("")
|
||||
.into_par_iter()
|
||||
.map(|slide| {
|
||||
text_svg_generator(slide, &Arc::clone(&fontdb))
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Couldn't create svg: {e}");
|
||||
})
|
||||
text_svg_generator(slide, &Arc::clone(&fontdb)).unwrap_or_else(|e| {
|
||||
panic!("Couldn't create svg: {e}");
|
||||
})
|
||||
})
|
||||
.collect::<Vec<Slide>>();
|
||||
let items = vec![
|
||||
|
|
@ -404,9 +364,7 @@ mod test {
|
|||
find_svgs(&items)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
Err(format!("Error in the loading process: {e}"))
|
||||
}
|
||||
Err(e) => Err(format!("Error in the loading process: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -511,8 +469,7 @@ mod test {
|
|||
let Ok(file) = fs::File::open(path) else {
|
||||
panic!("couldn't open file");
|
||||
};
|
||||
let Ok(size) = file.metadata().map(|data| data.len())
|
||||
else {
|
||||
let Ok(size) = file.metadata().map(|data| data.len()) else {
|
||||
panic!("couldn't get file metadata");
|
||||
};
|
||||
assert!(size > 0);
|
||||
|
|
|
|||
|
|
@ -14,9 +14,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Image {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
|
|
@ -91,22 +89,19 @@ impl From<&Value> for Image {
|
|||
fn from(value: &Value) -> Self {
|
||||
match value {
|
||||
Value::List(list) => {
|
||||
let path = if let Some(path_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("source"))
|
||||
}) {
|
||||
let path = if let Some(path_pos) = list
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("source")))
|
||||
{
|
||||
let pos = path_pos + 1;
|
||||
list.get(pos)
|
||||
.map(|p| PathBuf::from(String::from(p)))
|
||||
list.get(pos).map(|p| PathBuf::from(String::from(p)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let title = path.clone().map(|p| {
|
||||
let path =
|
||||
p.to_str().unwrap_or_default().to_string();
|
||||
let title =
|
||||
path.rsplit_once('/').unwrap_or_default().1;
|
||||
let path = p.to_str().unwrap_or_default().to_string();
|
||||
let title = path.rsplit_once('/').unwrap_or_default().1;
|
||||
title.to_string()
|
||||
});
|
||||
Self {
|
||||
|
|
@ -131,10 +126,7 @@ impl ServiceTrait for Image {
|
|||
|
||||
fn to_slides(&self) -> Result<Vec<Slide>> {
|
||||
let slide = SlideBuilder::new()
|
||||
.background(
|
||||
Background::try_from(self.path.clone())
|
||||
.into_diagnostic()?,
|
||||
)
|
||||
.background(Background::try_from(self.path.clone()).into_diagnostic()?)
|
||||
.text("")
|
||||
.audio("")
|
||||
.font("")
|
||||
|
|
@ -180,9 +172,7 @@ impl Model<Image> {
|
|||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"There was an error in converting images: {e}"
|
||||
);
|
||||
error!("There was an error in converting images: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -241,13 +231,13 @@ pub async fn add_image(
|
|||
.unwrap_or_default();
|
||||
|
||||
query!(
|
||||
r#"INSERT INTO images (title, file_path) VALUES ($1, $2)"#,
|
||||
image.title,
|
||||
path,
|
||||
)
|
||||
.execute(&*db)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
r#"INSERT INTO images (title, file_path) VALUES ($1, $2)"#,
|
||||
image.title,
|
||||
path,
|
||||
)
|
||||
.execute(&*db)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
current_images.push(image);
|
||||
}
|
||||
|
|
@ -271,8 +261,9 @@ pub async fn update_image(
|
|||
image.title,
|
||||
path,
|
||||
)
|
||||
.execute(&*db)
|
||||
.await.into_diagnostic()?;
|
||||
.execute(&*db)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
let current_image = images
|
||||
.iter()
|
||||
|
|
@ -288,10 +279,7 @@ pub async fn update_image(
|
|||
Ok(images)
|
||||
}
|
||||
|
||||
pub async fn get_from_db(
|
||||
database_id: i32,
|
||||
db: &mut SqliteConnection,
|
||||
) -> Result<Image> {
|
||||
pub async fn get_from_db(database_id: i32, 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()
|
||||
}
|
||||
|
||||
|
|
@ -303,9 +291,7 @@ mod test {
|
|||
fn test_image(title: String) -> Image {
|
||||
Image {
|
||||
title,
|
||||
path: PathBuf::from(
|
||||
"/home/chris/pics/memes/no-i-dont-think.gif",
|
||||
),
|
||||
path: PathBuf::from("/home/chris/pics/memes/no-i-dont-think.gif"),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -342,14 +328,8 @@ mod test {
|
|||
let new_image = test_image("A newer image".into());
|
||||
match result {
|
||||
Ok(()) => {
|
||||
assert_eq!(
|
||||
&image,
|
||||
image_model.find(|i| i.id == 0).expect("")
|
||||
);
|
||||
assert_ne!(
|
||||
&new_image,
|
||||
image_model.find(|i| i.id == 0).expect("")
|
||||
);
|
||||
assert_eq!(&image, image_model.find(|i| i.id == 0).expect(""));
|
||||
assert_ne!(&new_image, image_model.find(|i| i.id == 0).expect(""));
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("There was an error adding the image: {e:?}",)
|
||||
|
|
|
|||
|
|
@ -29,18 +29,10 @@ impl TryFrom<PathBuf> for ServiceItemKind {
|
|||
let ext = path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.ok_or_else(|| {
|
||||
miette::miette!(
|
||||
"There isn't an extension on this file"
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| miette::miette!("There isn't an extension on this file"))?;
|
||||
match ext {
|
||||
"png" | "jpg" | "jpeg" => {
|
||||
Ok(Self::Image(Image::from(path)))
|
||||
}
|
||||
"mp4" | "mkv" | "webm" => {
|
||||
Ok(Self::Video(Video::from(path)))
|
||||
}
|
||||
"png" | "jpg" | "jpeg" => Ok(Self::Image(Image::from(path))),
|
||||
"mp4" | "mkv" | "webm" => Ok(Self::Video(Video::from(path))),
|
||||
"pdf" => Ok(Self::Presentation(Presentation::from(path))),
|
||||
_ => Err(miette::miette!("Unknown item")),
|
||||
}
|
||||
|
|
@ -53,9 +45,7 @@ impl ServiceItemKind {
|
|||
Self::Song(song) => song.title.clone(),
|
||||
Self::Video(video) => video.title.clone(),
|
||||
Self::Image(image) => image.title.clone(),
|
||||
Self::Presentation(presentation) => {
|
||||
presentation.title.clone()
|
||||
}
|
||||
Self::Presentation(presentation) => presentation.title.clone(),
|
||||
Self::Content(_slide) => todo!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -65,9 +55,7 @@ impl ServiceItemKind {
|
|||
Self::Song(song) => song.to_service_item(),
|
||||
Self::Video(video) => video.to_service_item(),
|
||||
Self::Image(image) => image.to_service_item(),
|
||||
Self::Presentation(presentation) => {
|
||||
presentation.to_service_item()
|
||||
}
|
||||
Self::Presentation(presentation) => presentation.to_service_item(),
|
||||
Self::Content(_slide) => {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -112,9 +100,7 @@ impl From<ServiceItemKind> for String {
|
|||
ServiceItemKind::Song(_) => "song".to_owned(),
|
||||
ServiceItemKind::Video(_) => "video".to_owned(),
|
||||
ServiceItemKind::Image(_) => "image".to_owned(),
|
||||
ServiceItemKind::Presentation(_) => {
|
||||
"presentation".to_owned()
|
||||
}
|
||||
ServiceItemKind::Presentation(_) => "presentation".to_owned(),
|
||||
ServiceItemKind::Content(_) => "content".to_owned(),
|
||||
}
|
||||
}
|
||||
|
|
@ -128,10 +114,7 @@ pub enum ParseError {
|
|||
impl Error for ParseError {}
|
||||
|
||||
impl Display for ParseError {
|
||||
fn fmt(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let message = match self {
|
||||
Self::UnknownType => {
|
||||
"The type does not exist. It needs to be one of 'song', 'video', 'image', 'presentation', or 'content'"
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ pub struct Model<T> {
|
|||
pub kind: LibraryKind,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Copy, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, Serialize, Deserialize)]
|
||||
pub enum LibraryKind {
|
||||
Song,
|
||||
Video,
|
||||
|
|
@ -25,9 +23,7 @@ pub enum LibraryKind {
|
|||
Presentation,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct KindWrapper(pub (LibraryKind, i32));
|
||||
|
||||
impl From<PathBuf> for LibraryKind {
|
||||
|
|
@ -39,14 +35,10 @@ impl From<PathBuf> for LibraryKind {
|
|||
impl TryFrom<(Vec<u8>, String)> for KindWrapper {
|
||||
type Error = miette::Error;
|
||||
|
||||
fn try_from(
|
||||
value: (Vec<u8>, String),
|
||||
) -> std::result::Result<Self, Self::Error> {
|
||||
fn try_from(value: (Vec<u8>, String)) -> std::result::Result<Self, Self::Error> {
|
||||
let (data, mime) = value;
|
||||
match mime.as_str() {
|
||||
"application/service-item" => {
|
||||
ron::de::from_bytes(&data).into_diagnostic()
|
||||
}
|
||||
"application/service-item" => ron::de::from_bytes(&data).into_diagnostic(),
|
||||
_ => Err(miette!("Wrong mime type: {mime}")),
|
||||
}
|
||||
}
|
||||
|
|
@ -64,10 +56,7 @@ impl AsMimeTypes for KindWrapper {
|
|||
Cow::from(vec!["application/service-item".to_string()])
|
||||
}
|
||||
|
||||
fn as_bytes(
|
||||
&self,
|
||||
mime_type: &str,
|
||||
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||
fn as_bytes(&self, mime_type: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||
debug!(?self);
|
||||
debug!(mime_type);
|
||||
let ron = ron::ser::to_string(self).ok()?;
|
||||
|
|
@ -86,11 +75,7 @@ impl<T> Model<T> {
|
|||
todo!()
|
||||
}
|
||||
|
||||
pub fn update_item<P>(
|
||||
&mut self,
|
||||
item: T,
|
||||
predicate: P,
|
||||
) -> Result<()>
|
||||
pub fn update_item<P>(&mut self, item: T, predicate: P) -> Result<()>
|
||||
where
|
||||
P: Fn(&T) -> bool,
|
||||
{
|
||||
|
|
@ -98,7 +83,11 @@ impl<T> Model<T> {
|
|||
.iter()
|
||||
.position(predicate)
|
||||
.ok_or_else(|| miette!("Item cannot be found"))
|
||||
.map(|index| self.items.get_mut(index).expect("Since we found position this should always exist"))
|
||||
.map(|index| {
|
||||
self.items
|
||||
.get_mut(index)
|
||||
.expect("Since we found position this should always exist")
|
||||
})
|
||||
.map(|current_item| {
|
||||
let _old_item = replace(current_item, item);
|
||||
})
|
||||
|
|
@ -119,9 +108,8 @@ impl<T> Model<T> {
|
|||
|
||||
#[must_use]
|
||||
pub fn get_item(&self, index: i32) -> Option<&T> {
|
||||
self.items.get(
|
||||
usize::try_from(index).expect("shouldn't be negative"),
|
||||
)
|
||||
self.items
|
||||
.get(usize::try_from(index).expect("shouldn't be negative"))
|
||||
}
|
||||
|
||||
pub fn find<P>(&self, f: P) -> Option<&T>
|
||||
|
|
@ -131,11 +119,7 @@ impl<T> Model<T> {
|
|||
self.items.iter().find(f)
|
||||
}
|
||||
|
||||
pub fn insert_item(
|
||||
&mut self,
|
||||
item: T,
|
||||
index: usize,
|
||||
) -> Result<()> {
|
||||
pub fn insert_item(&mut self, item: T, index: usize) -> Result<()> {
|
||||
self.items.insert(index, item);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -153,8 +137,7 @@ impl<T> Model<T> {
|
|||
// }
|
||||
|
||||
pub async fn get_db() -> SqliteConnection {
|
||||
let mut data = dirs::data_local_dir()
|
||||
.expect("Should be able to find a data dir");
|
||||
let mut data = dirs::data_local_dir().expect("Should be able to find a data dir");
|
||||
data.push("lumina");
|
||||
let _ = fs::create_dir_all(&data);
|
||||
data.push("library-db.sqlite3");
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@ use super::kinds::ServiceItemKind;
|
|||
use super::model::{LibraryKind, Model};
|
||||
use super::service_items::ServiceTrait;
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PresKind {
|
||||
Html,
|
||||
Pdf {
|
||||
|
|
@ -147,20 +145,19 @@ impl From<&Value> for Presentation {
|
|||
fn from(value: &Value) -> Self {
|
||||
match value {
|
||||
Value::List(list) => {
|
||||
let path = if let Some(path_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("source"))
|
||||
}) {
|
||||
let path = if let Some(path_pos) = list
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("source")))
|
||||
{
|
||||
let pos = path_pos + 1;
|
||||
list.get(pos)
|
||||
.map(|p| PathBuf::from(String::from(p)))
|
||||
list.get(pos).map(|p| PathBuf::from(String::from(p)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let title = path.clone().map(|p| {
|
||||
p.to_str().unwrap_or_default().to_string()
|
||||
});
|
||||
let title = path
|
||||
.clone()
|
||||
.map(|p| p.to_str().unwrap_or_default().to_string());
|
||||
Self {
|
||||
title: title.unwrap_or_default(),
|
||||
path: path.unwrap_or_default(),
|
||||
|
|
@ -188,23 +185,18 @@ impl ServiceTrait for Presentation {
|
|||
ending_index,
|
||||
} = self.kind
|
||||
else {
|
||||
return Err(miette::miette!(
|
||||
"This is not a pdf presentation"
|
||||
));
|
||||
return Err(miette::miette!("This is not a pdf presentation"));
|
||||
};
|
||||
let background = Background::try_from(self.path.clone())
|
||||
.into_diagnostic()?;
|
||||
let background = Background::try_from(self.path.clone()).into_diagnostic()?;
|
||||
debug!(?background);
|
||||
let document = Document::open(background.path.as_path())
|
||||
.into_diagnostic()?;
|
||||
let document = Document::open(background.path.as_path()).into_diagnostic()?;
|
||||
debug!(?document);
|
||||
let pages = document.pages().into_diagnostic()?;
|
||||
debug!(?pages);
|
||||
let pages: Vec<Handle> = pages
|
||||
.enumerate()
|
||||
.filter_map(|(index, page)| {
|
||||
let index = i32::try_from(index)
|
||||
.expect("Shouldn't be that high");
|
||||
let index = i32::try_from(index).expect("Shouldn't be that high");
|
||||
|
||||
if index < starting_index || index > ending_index {
|
||||
return None;
|
||||
|
|
@ -232,10 +224,7 @@ impl ServiceTrait for Presentation {
|
|||
let mut slides: Vec<Slide> = vec![];
|
||||
for (index, page) in pages.into_iter().enumerate() {
|
||||
let slide = SlideBuilder::new()
|
||||
.background(
|
||||
Background::try_from(self.path.clone())
|
||||
.into_diagnostic()?,
|
||||
)
|
||||
.background(Background::try_from(self.path.clone()).into_diagnostic()?)
|
||||
.text("")
|
||||
.audio("")
|
||||
.font("")
|
||||
|
|
@ -244,10 +233,7 @@ impl ServiceTrait for Presentation {
|
|||
.video_loop(false)
|
||||
.video_start_time(0.0)
|
||||
.video_end_time(0.0)
|
||||
.pdf_index(
|
||||
u32::try_from(index)
|
||||
.expect("Shouldn't get that high"),
|
||||
)
|
||||
.pdf_index(u32::try_from(index).expect("Shouldn't get that high"))
|
||||
.pdf_page(page)
|
||||
.build()?;
|
||||
slides.push(slide);
|
||||
|
|
@ -323,26 +309,17 @@ impl Model<Presentation> {
|
|||
path: presentation.path.clone().into(),
|
||||
kind: if presentation.html {
|
||||
PresKind::Html
|
||||
} else if let (
|
||||
Some(starting_index),
|
||||
Some(ending_index),
|
||||
) = (
|
||||
presentation.starting_index,
|
||||
presentation.ending_index,
|
||||
) {
|
||||
} else if let (Some(starting_index), Some(ending_index)) =
|
||||
(presentation.starting_index, presentation.ending_index)
|
||||
{
|
||||
PresKind::Pdf {
|
||||
starting_index: i32::try_from(
|
||||
starting_index,
|
||||
)
|
||||
.expect("Shouldn't get that high"),
|
||||
ending_index: i32::try_from(
|
||||
ending_index,
|
||||
)
|
||||
.expect("Shouldn't get that high"),
|
||||
starting_index: i32::try_from(starting_index)
|
||||
.expect("Shouldn't get that high"),
|
||||
ending_index: i32::try_from(ending_index)
|
||||
.expect("Shouldn't get that high"),
|
||||
}
|
||||
} else {
|
||||
let path =
|
||||
PathBuf::from(presentation.path);
|
||||
let path = PathBuf::from(presentation.path);
|
||||
|
||||
Document::open(path.as_path()).map_or(
|
||||
PresKind::Generic,
|
||||
|
|
@ -353,8 +330,7 @@ impl Model<Presentation> {
|
|||
ending_index: 0,
|
||||
},
|
||||
|count| {
|
||||
let ending_index =
|
||||
count - 1;
|
||||
let ending_index = count - 1;
|
||||
PresKind::Pdf {
|
||||
starting_index: 0,
|
||||
ending_index,
|
||||
|
|
@ -367,9 +343,7 @@ impl Model<Presentation> {
|
|||
});
|
||||
}
|
||||
}
|
||||
Err(e) => error!(
|
||||
"There was an error in converting presentations: {e}"
|
||||
),
|
||||
Err(e) => error!("There was an error in converting presentations: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -381,9 +355,7 @@ pub async fn remove_presentations(
|
|||
) -> Result<Vec<Presentation>> {
|
||||
let presentations = presentations
|
||||
.into_iter()
|
||||
.filter(|current_presentation| {
|
||||
!ids.contains(¤t_presentation.id)
|
||||
})
|
||||
.filter(|current_presentation| !ids.contains(¤t_presentation.id))
|
||||
.collect();
|
||||
|
||||
let delete = format!(
|
||||
|
|
@ -411,12 +383,8 @@ pub async fn remove_presentation(
|
|||
|
||||
let index = presentations
|
||||
.iter()
|
||||
.position(|current_presentation| {
|
||||
current_presentation.id == id
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
miette!("Could not find presentation in model")
|
||||
})?;
|
||||
.position(|current_presentation| current_presentation.id == id)
|
||||
.ok_or_else(|| miette!("Could not find presentation in model"))?;
|
||||
presentations.remove(index);
|
||||
Ok(presentations)
|
||||
}
|
||||
|
|
@ -473,8 +441,7 @@ pub async fn update_presentation(
|
|||
let (starting_index, ending_index) = if let PresKind::Pdf {
|
||||
starting_index: s_index,
|
||||
ending_index: e_index,
|
||||
} =
|
||||
presentation.get_kind()
|
||||
} = presentation.get_kind()
|
||||
{
|
||||
(*s_index, *e_index)
|
||||
} else {
|
||||
|
|
@ -496,12 +463,8 @@ pub async fn update_presentation(
|
|||
|
||||
let current_presentation = presentations
|
||||
.iter()
|
||||
.position(|current_presentation| {
|
||||
current_presentation.id == presentation.id
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
miette!("Could not find presentation in model")
|
||||
})
|
||||
.position(|current_presentation| current_presentation.id == presentation.id)
|
||||
.ok_or_else(|| miette!("Could not find presentation in model"))
|
||||
.map(|index| {
|
||||
presentations
|
||||
.get_mut(index)
|
||||
|
|
@ -556,9 +519,7 @@ mod test {
|
|||
};
|
||||
let db = Arc::new(add_db().await.expect("Getting db error"));
|
||||
presentation_model.load_from_db(db).await;
|
||||
if let Some(presentation) =
|
||||
presentation_model.find(|p| p.id == 4)
|
||||
{
|
||||
if let Some(presentation) = presentation_model.find(|p| p.id == 4) {
|
||||
let test_presentation = test_presentation();
|
||||
assert_eq!(&test_presentation, presentation);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -45,9 +45,7 @@ impl Ord for ServiceItem {
|
|||
impl TryFrom<(Vec<u8>, String)> for ServiceItem {
|
||||
type Error = miette::Error;
|
||||
|
||||
fn try_from(
|
||||
value: (Vec<u8>, String),
|
||||
) -> std::result::Result<Self, Self::Error> {
|
||||
fn try_from(value: (Vec<u8>, String)) -> std::result::Result<Self, Self::Error> {
|
||||
let (data, mime) = value;
|
||||
debug!(?mime);
|
||||
ron::de::from_bytes(&data).into_diagnostic()
|
||||
|
|
@ -70,10 +68,7 @@ impl AsMimeTypes for ServiceItem {
|
|||
Cow::from(vec!["application/service-item".to_string()])
|
||||
}
|
||||
|
||||
fn as_bytes(
|
||||
&self,
|
||||
mime_type: &str,
|
||||
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||
fn as_bytes(&self, mime_type: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||
debug!(?self);
|
||||
debug!(mime_type);
|
||||
let ron = ron::ser::to_string(self).ok()?;
|
||||
|
|
@ -89,18 +84,10 @@ impl TryFrom<PathBuf> for ServiceItem {
|
|||
let ext = path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.ok_or_else(|| {
|
||||
miette::miette!(
|
||||
"There isn't an extension on this file"
|
||||
)
|
||||
})?;
|
||||
.ok_or_else(|| miette::miette!("There isn't an extension on this file"))?;
|
||||
match ext {
|
||||
"png" | "jpg" | "jpeg" => {
|
||||
Ok(Self::from(&Image::from(path)))
|
||||
}
|
||||
"mp4" | "mkv" | "webm" => {
|
||||
Ok(Self::from(&Video::from(path)))
|
||||
}
|
||||
"png" | "jpg" | "jpeg" => Ok(Self::from(&Image::from(path))),
|
||||
"mp4" | "mkv" | "webm" => Ok(Self::from(&Video::from(path))),
|
||||
_ => Err(miette!("Unkown service item")),
|
||||
}
|
||||
}
|
||||
|
|
@ -112,9 +99,7 @@ impl From<&ServiceItem> for Value {
|
|||
ServiceItemKind::Song(song) => Self::from(song),
|
||||
ServiceItemKind::Video(video) => Self::from(video),
|
||||
ServiceItemKind::Image(image) => Self::from(image),
|
||||
ServiceItemKind::Presentation(presentation) => {
|
||||
Self::from(presentation)
|
||||
}
|
||||
ServiceItemKind::Presentation(presentation) => Self::from(presentation),
|
||||
ServiceItemKind::Content(slide) => Self::from(slide),
|
||||
}
|
||||
}
|
||||
|
|
@ -130,12 +115,8 @@ impl ServiceItem {
|
|||
ServiceItemKind::Song(song) => song.to_slides(),
|
||||
ServiceItemKind::Video(video) => video.to_slides(),
|
||||
ServiceItemKind::Image(image) => image.to_slides(),
|
||||
ServiceItemKind::Presentation(presentation) => {
|
||||
presentation.to_slides()
|
||||
}
|
||||
ServiceItemKind::Content(slide) => {
|
||||
Ok(vec![slide.clone()])
|
||||
}
|
||||
ServiceItemKind::Presentation(presentation) => presentation.to_slides(),
|
||||
ServiceItemKind::Content(slide) => Ok(vec![slide.clone()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -177,70 +158,44 @@ impl From<&Value> for ServiceItem {
|
|||
_ => false,
|
||||
})
|
||||
.map_or_else(|| 1, |pos| pos + 1);
|
||||
if let Some(_content) =
|
||||
list.iter().position(|v| match v {
|
||||
Value::List(list)
|
||||
if list.iter().next()
|
||||
== Some(&Value::Symbol(
|
||||
Symbol("text".into()),
|
||||
)) =>
|
||||
{
|
||||
list.iter().next().is_some()
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
{
|
||||
if let Some(_content) = list.iter().position(|v| match v {
|
||||
Value::List(list)
|
||||
if list.iter().next()
|
||||
== Some(&Value::Symbol(Symbol("text".into()))) =>
|
||||
{
|
||||
list.iter().next().is_some()
|
||||
}
|
||||
_ => false,
|
||||
}) {
|
||||
let slide = Slide::from(value);
|
||||
let title = slide.text();
|
||||
Self {
|
||||
id: 0,
|
||||
title,
|
||||
database_id: 0,
|
||||
kind: ServiceItemKind::Content(
|
||||
slide.clone(),
|
||||
),
|
||||
kind: ServiceItemKind::Content(slide.clone()),
|
||||
slides: vec![slide],
|
||||
}
|
||||
} else if let Some(background) =
|
||||
list.get(background_pos)
|
||||
{
|
||||
} else if let Some(background) = list.get(background_pos) {
|
||||
if let Value::List(item) = background {
|
||||
match &item[0] {
|
||||
Value::Symbol(Symbol(s))
|
||||
if s == "image" =>
|
||||
{
|
||||
Self::from(&Image::from(
|
||||
background,
|
||||
))
|
||||
Value::Symbol(Symbol(s)) if s == "image" => {
|
||||
Self::from(&Image::from(background))
|
||||
}
|
||||
Value::Symbol(Symbol(s))
|
||||
if s == "video" =>
|
||||
{
|
||||
Self::from(&Video::from(
|
||||
background,
|
||||
))
|
||||
Value::Symbol(Symbol(s)) if s == "video" => {
|
||||
Self::from(&Video::from(background))
|
||||
}
|
||||
Value::Symbol(Symbol(s))
|
||||
if s == "presentation" =>
|
||||
{
|
||||
Self::from(&Presentation::from(
|
||||
background,
|
||||
))
|
||||
Value::Symbol(Symbol(s)) if s == "presentation" => {
|
||||
Self::from(&Presentation::from(background))
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
"There is no background here: {:?}",
|
||||
background
|
||||
);
|
||||
error!("There is no background here: {:?}", background);
|
||||
Self::default()
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
"There is no background here: {:?}",
|
||||
background_pos
|
||||
);
|
||||
error!("There is no background here: {:?}", background_pos);
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -346,9 +301,7 @@ impl From<&Presentation> for ServiceItem {
|
|||
fn from(presentation: &Presentation) -> Self {
|
||||
match presentation.to_slides() {
|
||||
Ok(slides) => Self {
|
||||
kind: ServiceItemKind::Presentation(
|
||||
presentation.clone(),
|
||||
),
|
||||
kind: ServiceItemKind::Presentation(presentation.clone()),
|
||||
database_id: presentation.id,
|
||||
title: presentation.title.clone(),
|
||||
slides,
|
||||
|
|
@ -357,9 +310,7 @@ impl From<&Presentation> for ServiceItem {
|
|||
Err(e) => {
|
||||
error!(?e);
|
||||
Self {
|
||||
kind: ServiceItemKind::Presentation(
|
||||
presentation.clone(),
|
||||
),
|
||||
kind: ServiceItemKind::Presentation(presentation.clone()),
|
||||
database_id: presentation.id,
|
||||
title: presentation.title.clone(),
|
||||
..Default::default()
|
||||
|
|
@ -410,10 +361,7 @@ impl Clone for Box<dyn ServiceTrait> {
|
|||
}
|
||||
|
||||
impl std::fmt::Debug for Box<dyn ServiceTrait> {
|
||||
fn fmt(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> Result<(), std::fmt::Error> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(f, "{}: {}", self.id(), self.title())
|
||||
}
|
||||
}
|
||||
|
|
@ -453,14 +401,8 @@ mod test {
|
|||
let pres_item = ServiceItem::from(&pres);
|
||||
let mut service_model = Service::default();
|
||||
service_model.add_item(&song);
|
||||
assert_eq!(
|
||||
ServiceItemKind::Song(song),
|
||||
service_model.items[0].kind
|
||||
);
|
||||
assert_eq!(
|
||||
ServiceItemKind::Presentation(pres),
|
||||
pres_item.kind
|
||||
);
|
||||
assert_eq!(ServiceItemKind::Song(song), service_model.items[0].kind);
|
||||
assert_eq!(ServiceItemKind::Presentation(pres), pres_item.kind);
|
||||
assert_eq!(service_item, service_model.items[0]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ use std::path::PathBuf;
|
|||
|
||||
pub const SETTINGS_VERSION: u64 = 1;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize,
|
||||
)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum AppTheme {
|
||||
Dark,
|
||||
Light,
|
||||
|
|
@ -28,15 +26,7 @@ impl AppTheme {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
CosmicConfigEntry,
|
||||
Debug,
|
||||
Deserialize,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
)]
|
||||
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub app_theme: AppTheme,
|
||||
|
|
@ -55,14 +45,7 @@ impl Default for Settings {
|
|||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
CosmicConfigEntry,
|
||||
Debug,
|
||||
Deserialize,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Default,
|
||||
Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize, Default,
|
||||
)]
|
||||
pub struct PersistentState {
|
||||
pub recent_files: VecDeque<PathBuf>,
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@ use std::fmt::Display;
|
|||
use std::path::{Path, PathBuf};
|
||||
use tracing::error;
|
||||
|
||||
use crate::ui::gst_video;
|
||||
use crate::ui::text_svg::{Color, Font, Shadow, Stroke, TextSvg};
|
||||
|
||||
use super::songs::Song;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Slide {
|
||||
id: i32,
|
||||
pub(crate) background: Background,
|
||||
|
|
@ -39,9 +38,7 @@ pub struct Slide {
|
|||
pdf_page: Option<Handle>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BackgroundKind {
|
||||
#[default]
|
||||
Image,
|
||||
|
|
@ -50,17 +47,7 @@ pub enum BackgroundKind {
|
|||
Html,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Hash,
|
||||
)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum TextAlignment {
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
|
|
@ -90,9 +77,7 @@ impl From<&Value> for TextAlignment {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Background {
|
||||
pub path: PathBuf,
|
||||
pub kind: BackgroundKind,
|
||||
|
|
@ -103,9 +88,7 @@ pub struct Background {
|
|||
impl TryFrom<&Background> for Video {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(
|
||||
value: &Background,
|
||||
) -> std::result::Result<Self, Self::Error> {
|
||||
fn try_from(value: &Background) -> std::result::Result<Self, Self::Error> {
|
||||
Self::new(
|
||||
&url::Url::from_file_path(value.path.clone())
|
||||
.map_err(|()| ParseError::BackgroundNotVideo)?,
|
||||
|
|
@ -117,14 +100,16 @@ impl TryFrom<&Background> for Video {
|
|||
impl TryFrom<Background> for Video {
|
||||
type Error = ParseError;
|
||||
|
||||
fn try_from(
|
||||
value: Background,
|
||||
) -> std::result::Result<Self, Self::Error> {
|
||||
Self::new(
|
||||
&url::Url::from_file_path(value.path)
|
||||
.map_err(|()| ParseError::BackgroundNotVideo)?,
|
||||
)
|
||||
.map_err(|_| ParseError::BackgroundNotVideo)
|
||||
fn try_from(value: Background) -> std::result::Result<Self, Self::Error> {
|
||||
let url = &url::Url::from_file_path(value.path)
|
||||
.map_err(|()| ParseError::BackgroundNotVideo)?;
|
||||
|
||||
let settings = gst_video::VideoSettings {
|
||||
mute: true,
|
||||
framerate: 30,
|
||||
};
|
||||
gst_video::create_video(url, &settings)
|
||||
.map_err(|_| ParseError::BackgroundNotVideo)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +124,7 @@ impl TryFrom<PathBuf> for Background {
|
|||
type Error = ParseError;
|
||||
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
|
||||
let path = if path.starts_with("~") {
|
||||
let path = path
|
||||
.to_str()
|
||||
.expect("Should have a string")
|
||||
.to_string();
|
||||
let path = path.to_str().expect("Should have a string").to_string();
|
||||
let path = path.trim_start_matches("file://");
|
||||
let home = dirs::home_dir()
|
||||
.expect("We should have a home directory")
|
||||
|
|
@ -237,21 +219,12 @@ pub enum ParseError {
|
|||
impl std::error::Error for ParseError {}
|
||||
|
||||
impl Display for ParseError {
|
||||
fn fmt(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let message = match self {
|
||||
Self::NonBackgroundFile => {
|
||||
"The file is not a recognized image or video type"
|
||||
}
|
||||
Self::NonBackgroundFile => "The file is not a recognized image or video type",
|
||||
Self::DoesNotExist => "This file doesn't exist",
|
||||
Self::CannotCanonicalize => {
|
||||
"Could not canonicalize this file"
|
||||
}
|
||||
Self::BackgroundNotVideo => {
|
||||
"This background isn't a video"
|
||||
}
|
||||
Self::CannotCanonicalize => "Could not canonicalize this file",
|
||||
Self::BackgroundNotVideo => "This background isn't a video",
|
||||
};
|
||||
write!(f, "Error: {message}")
|
||||
}
|
||||
|
|
@ -398,9 +371,7 @@ impl Slide {
|
|||
.background(song.background.unwrap_or_default())
|
||||
.font(song.font.unwrap_or_default())
|
||||
.font_size(song.font_size.unwrap_or_default())
|
||||
.text_alignment(
|
||||
song.text_alignment.unwrap_or_default(),
|
||||
)
|
||||
.text_alignment(song.text_alignment.unwrap_or_default())
|
||||
.audio(song.audio.unwrap_or_default())
|
||||
.video_loop(true)
|
||||
.video_start_time(0.0)
|
||||
|
|
@ -444,10 +415,10 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
|||
const DEFAULT_TEXT_LOCATION: usize = 0;
|
||||
|
||||
let mut slide = SlideBuilder::new();
|
||||
let background_position = if let Some(background) =
|
||||
lisp.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("background"))
|
||||
}) {
|
||||
let background_position = if let Some(background) = lisp
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("background")))
|
||||
{
|
||||
background + 1
|
||||
} else {
|
||||
DEFAULT_BACKGROUND_LOCATION
|
||||
|
|
@ -461,8 +432,7 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
|||
|
||||
let text_position = lisp.iter().position(|v| match v {
|
||||
Value::List(vec) => {
|
||||
vec[DEFAULT_TEXT_LOCATION]
|
||||
== Value::Symbol(Symbol::from("text"))
|
||||
vec[DEFAULT_TEXT_LOCATION] == Value::Symbol(Symbol::from("text"))
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
|
@ -507,14 +477,11 @@ fn lisp_to_slide(lisp: &[Value]) -> Slide {
|
|||
fn lisp_to_font_size(lisp: &Value) -> i32 {
|
||||
match lisp {
|
||||
Value::List(list) => {
|
||||
if let Some(font_size_position) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("font-size"))
|
||||
})
|
||||
if let Some(font_size_position) = list
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("font-size")))
|
||||
{
|
||||
if let Some(font_size_value) =
|
||||
list.get(font_size_position + 1)
|
||||
{
|
||||
if let Some(font_size_value) = list.get(font_size_position + 1) {
|
||||
font_size_value.into()
|
||||
} else {
|
||||
50
|
||||
|
|
@ -541,9 +508,10 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
|||
match lisp {
|
||||
Value::List(list) => {
|
||||
let _kind = list[0].clone();
|
||||
if let Some(source) = list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("source"))
|
||||
}) {
|
||||
if let Some(source) = list
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("source")))
|
||||
{
|
||||
let source = &list[source + 1];
|
||||
match source {
|
||||
Value::String(s) => {
|
||||
|
|
@ -561,9 +529,7 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
|||
match Background::try_from(s.as_str()) {
|
||||
Ok(background) => background,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Couldn't load background: {e}"
|
||||
);
|
||||
error!("Couldn't load background: {e}");
|
||||
Background::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -571,9 +537,7 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
|||
match Background::try_from(s.as_str()) {
|
||||
Ok(background) => background,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Couldn't load background: {e}"
|
||||
);
|
||||
error!("Couldn't load background: {e}");
|
||||
Background::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -589,9 +553,7 @@ pub fn lisp_to_background(lisp: &Value) -> Background {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SlideBuilder {
|
||||
background: Option<Background>,
|
||||
text: Option<String>,
|
||||
|
|
@ -626,10 +588,7 @@ impl SlideBuilder {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
pub(crate) fn background(
|
||||
mut self,
|
||||
background: Background,
|
||||
) -> Self {
|
||||
pub(crate) fn background(mut self, background: Background) -> Self {
|
||||
let _ = self.background.insert(background);
|
||||
self
|
||||
}
|
||||
|
|
@ -639,10 +598,7 @@ impl SlideBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn text_color(
|
||||
mut self,
|
||||
text_color: impl Into<Color>,
|
||||
) -> Self {
|
||||
pub(crate) fn text_color(mut self, text_color: impl Into<Color>) -> Self {
|
||||
let _ = self.text_color.insert(text_color.into());
|
||||
self
|
||||
}
|
||||
|
|
@ -667,26 +623,17 @@ impl SlideBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn stroke(
|
||||
mut self,
|
||||
stroke: impl Into<Stroke>,
|
||||
) -> Self {
|
||||
pub(crate) fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
let _ = self.stroke.insert(stroke.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn shadow(
|
||||
mut self,
|
||||
shadow: impl Into<Shadow>,
|
||||
) -> Self {
|
||||
pub(crate) fn shadow(mut self, shadow: impl Into<Shadow>) -> Self {
|
||||
let _ = self.shadow.insert(shadow.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn text_alignment(
|
||||
mut self,
|
||||
text_alignment: TextAlignment,
|
||||
) -> Self {
|
||||
pub(crate) fn text_alignment(mut self, text_alignment: TextAlignment) -> Self {
|
||||
let _ = self.text_alignment.insert(text_alignment);
|
||||
self
|
||||
}
|
||||
|
|
@ -696,26 +643,17 @@ impl SlideBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn video_start_time(
|
||||
mut self,
|
||||
video_start_time: f32,
|
||||
) -> Self {
|
||||
pub(crate) fn video_start_time(mut self, video_start_time: f32) -> Self {
|
||||
let _ = self.video_start_time.insert(video_start_time);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn video_end_time(
|
||||
mut self,
|
||||
video_end_time: f32,
|
||||
) -> Self {
|
||||
pub(crate) fn video_end_time(mut self, video_end_time: f32) -> Self {
|
||||
let _ = self.video_end_time.insert(video_end_time);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn text_svg(
|
||||
mut self,
|
||||
text_svg: impl Into<TextSvg>,
|
||||
) -> Self {
|
||||
pub(crate) fn text_svg(mut self, text_svg: impl Into<TextSvg>) -> Self {
|
||||
let _ = self.text_svg.insert(text_svg.into());
|
||||
self
|
||||
}
|
||||
|
|
@ -725,10 +663,7 @@ impl SlideBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub(crate) fn pdf_index(
|
||||
mut self,
|
||||
pdf_index: impl Into<u32>,
|
||||
) -> Self {
|
||||
pub(crate) fn pdf_index(mut self, pdf_index: impl Into<u32>) -> Self {
|
||||
let _ = self.pdf_index.insert(pdf_index.into());
|
||||
self
|
||||
}
|
||||
|
|
@ -786,8 +721,7 @@ mod test {
|
|||
fn test_slide() -> Slide {
|
||||
Slide {
|
||||
text: "This is frodo".to_string(),
|
||||
background: Background::try_from("~/pics/frodo.jpg")
|
||||
.expect(""),
|
||||
background: Background::try_from("~/pics/frodo.jpg").expect(""),
|
||||
font: Some("Quicksand".to_string().into()),
|
||||
font_size: 140,
|
||||
..Default::default()
|
||||
|
|
@ -797,10 +731,7 @@ mod test {
|
|||
fn test_second_slide() -> Slide {
|
||||
Slide {
|
||||
text: String::new(),
|
||||
background: Background::try_from(
|
||||
"~/vids/test/camprules2024.mp4",
|
||||
)
|
||||
.expect(""),
|
||||
background: Background::try_from("~/vids/test/camprules2024.mp4").expect(""),
|
||||
font: Some("Quicksand".to_string().into()),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -808,8 +739,8 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_ron_deserialize() {
|
||||
let slide = read_to_string("./test_presentation.ron")
|
||||
.expect("Problem getting file read");
|
||||
let slide =
|
||||
read_to_string("./test_presentation.ron").expect("Problem getting file read");
|
||||
if let Err(e) = ron::from_str::<Vec<Slide>>(&slide) {
|
||||
panic!("{e:?}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,15 +16,7 @@ use std::fmt::Display;
|
|||
use tracing::error;
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct OnlineSong {
|
||||
pub lyrics: String,
|
||||
|
|
@ -35,15 +27,7 @@ pub struct OnlineSong {
|
|||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum Provider {
|
||||
Genius {
|
||||
|
|
@ -54,10 +38,7 @@ pub enum Provider {
|
|||
}
|
||||
|
||||
impl Display for Provider {
|
||||
fn fmt(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
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"),
|
||||
|
|
@ -67,13 +48,8 @@ impl Display for Provider {
|
|||
|
||||
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()
|
||||
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 })
|
||||
|
|
@ -103,14 +79,11 @@ impl From<OnlineSong> for Song {
|
|||
}
|
||||
|
||||
#[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()?;
|
||||
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);
|
||||
|
|
@ -160,16 +133,12 @@ fn parse_verse_name(line: &str) -> IResult<&str, VerseName> {
|
|||
Ok((input, verse_name))
|
||||
}
|
||||
|
||||
pub async fn search_genius(
|
||||
query: String,
|
||||
auth_token: String,
|
||||
) -> Result<Vec<OnlineSong>> {
|
||||
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 head_value = header::HeaderValue::from_str(&auth_token).into_diagnostic()?;
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(header::AUTHORIZATION, head_value);
|
||||
let client = reqwest::Client::builder()
|
||||
|
|
@ -186,8 +155,7 @@ pub async fn search_genius(
|
|||
.text()
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
let json: Value =
|
||||
serde_json::from_str(&response).into_diagnostic()?;
|
||||
let json: Value = serde_json::from_str(&response).into_diagnostic()?;
|
||||
let hits = json
|
||||
.get("response")
|
||||
.expect("respose")
|
||||
|
|
@ -196,52 +164,48 @@ pub async fn search_genius(
|
|||
.as_array()
|
||||
.expect("array");
|
||||
let songs: Vec<Option<OnlineSong>> =
|
||||
cosmic::iced::futures::future::join_all(hits.iter().map(
|
||||
|hit| async {
|
||||
let result = hit.get("result").expect("result");
|
||||
let title = result
|
||||
.get("title")
|
||||
.expect("title")
|
||||
.as_str()
|
||||
.expect("title")
|
||||
.to_string();
|
||||
let title = title.replace("\u{a0}", " ");
|
||||
let author = result
|
||||
.get("artist_names")
|
||||
.expect("artists")
|
||||
.as_str()
|
||||
.expect("artists")
|
||||
.to_string();
|
||||
let link = result
|
||||
.get("url")
|
||||
.expect("url")
|
||||
.as_str()
|
||||
.expect("url")
|
||||
.to_string();
|
||||
let song = OnlineSong {
|
||||
lyrics: String::new(),
|
||||
title,
|
||||
author,
|
||||
provider: Provider::Genius { parsable: false },
|
||||
link,
|
||||
};
|
||||
cosmic::iced::futures::future::join_all(hits.iter().map(|hit| async {
|
||||
let result = hit.get("result").expect("result");
|
||||
let title = result
|
||||
.get("title")
|
||||
.expect("title")
|
||||
.as_str()
|
||||
.expect("title")
|
||||
.to_string();
|
||||
let title = title.replace("\u{a0}", " ");
|
||||
let author = result
|
||||
.get("artist_names")
|
||||
.expect("artists")
|
||||
.as_str()
|
||||
.expect("artists")
|
||||
.to_string();
|
||||
let link = result
|
||||
.get("url")
|
||||
.expect("url")
|
||||
.as_str()
|
||||
.expect("url")
|
||||
.to_string();
|
||||
let song = OnlineSong {
|
||||
lyrics: String::new(),
|
||||
title,
|
||||
author,
|
||||
provider: Provider::Genius { parsable: false },
|
||||
link,
|
||||
};
|
||||
|
||||
match get_genius_lyrics(song).await {
|
||||
Ok(song) => Some(song),
|
||||
Err(e) => {
|
||||
error!("Couldn't get lyrics: {e}");
|
||||
None
|
||||
}
|
||||
match get_genius_lyrics(song).await {
|
||||
Ok(song) => Some(song),
|
||||
Err(e) => {
|
||||
error!("Couldn't get lyrics: {e}");
|
||||
None
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
Ok(songs.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
pub async fn get_genius_lyrics(
|
||||
mut song: OnlineSong,
|
||||
) -> Result<OnlineSong> {
|
||||
pub async fn get_genius_lyrics(mut song: OnlineSong) -> Result<OnlineSong> {
|
||||
let html = reqwest::get(&song.link)
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
|
|
@ -251,17 +215,15 @@ pub async fn get_genius_lyrics(
|
|||
.await
|
||||
.into_diagnostic()?;
|
||||
let document = scraper::Html::parse_document(&html);
|
||||
let Ok(lyrics_root_selector) = scraper::Selector::parse(
|
||||
r#"div[data-lyrics-container="true"]"#,
|
||||
) else {
|
||||
let Ok(lyrics_root_selector) =
|
||||
scraper::Selector::parse(r#"div[data-lyrics-container="true"]"#)
|
||||
else {
|
||||
return Err(miette!("error in finding lyrics_root"));
|
||||
};
|
||||
|
||||
let lyrics = document
|
||||
.select(&lyrics_root_selector)
|
||||
.filter(|element| {
|
||||
element.attr("data-exclude-from-selection").is_none()
|
||||
})
|
||||
.filter(|element| element.attr("data-exclude-from-selection").is_none())
|
||||
.filter(|element| {
|
||||
!element.value().classes().any(|class| {
|
||||
class.contains("Contrib")
|
||||
|
|
@ -278,11 +240,7 @@ pub async fn get_genius_lyrics(
|
|||
line_broken
|
||||
.root_element()
|
||||
.descendent_elements()
|
||||
.filter(|element| {
|
||||
element
|
||||
.attr("data-exclude-from-selection")
|
||||
.is_none()
|
||||
})
|
||||
.filter(|element| element.attr("data-exclude-from-selection").is_none())
|
||||
.filter(|element| {
|
||||
let element_name = element.value().name();
|
||||
element_name != "div" && element_name != "path"
|
||||
|
|
@ -307,9 +265,7 @@ pub async fn get_genius_lyrics(
|
|||
|| {
|
||||
lyrics.find("</div></div></div>").map_or_else(
|
||||
|| lyrics.clone(),
|
||||
|position| {
|
||||
lyrics.split_at(position + 18).1.to_string()
|
||||
},
|
||||
|position| lyrics.split_at(position + 18).1.to_string(),
|
||||
)
|
||||
},
|
||||
|position| lyrics.split_at(position).1.to_string(),
|
||||
|
|
@ -324,20 +280,17 @@ pub async fn get_genius_lyrics(
|
|||
pub async fn search_lyrics_com_links(
|
||||
query: impl AsRef<str> + std::fmt::Display,
|
||||
) -> Result<Vec<String>> {
|
||||
let html =
|
||||
reqwest::get(format!("http://www.lyrics.com/lyrics/{query}"))
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
.error_for_status()
|
||||
.into_diagnostic()?
|
||||
.text()
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
let html = reqwest::get(format!("http://www.lyrics.com/lyrics/{query}"))
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
.error_for_status()
|
||||
.into_diagnostic()?
|
||||
.text()
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
let document = scraper::Html::parse_document(&html);
|
||||
let Ok(best_matches_selector) =
|
||||
scraper::Selector::parse(".best-matches")
|
||||
else {
|
||||
let Ok(best_matches_selector) = scraper::Selector::parse(".best-matches") else {
|
||||
return Err(miette!("error in finding matches"));
|
||||
};
|
||||
let Ok(lyric_selector) = scraper::Selector::parse("a") else {
|
||||
|
|
@ -347,9 +300,7 @@ pub async fn search_lyrics_com_links(
|
|||
Ok(document
|
||||
.select(&best_matches_selector)
|
||||
.flat_map(|best_section| best_section.select(&lyric_selector))
|
||||
.map(|a| {
|
||||
a.value().attr("href").unwrap_or("").trim().to_string()
|
||||
})
|
||||
.map(|a| a.value().attr("href").unwrap_or("").trim().to_string())
|
||||
.filter(|a| a.contains("/lyric/"))
|
||||
.dedup()
|
||||
.map(|link| {
|
||||
|
|
@ -389,9 +340,7 @@ pub async fn lyrics_com_link_to_song(
|
|||
.into_diagnostic()?;
|
||||
|
||||
let document = scraper::Html::parse_document(&html);
|
||||
let Ok(lyric_selector) =
|
||||
scraper::Selector::parse(".lyric-body")
|
||||
else {
|
||||
let Ok(lyric_selector) = scraper::Selector::parse(".lyric-body") else {
|
||||
return Err(miette!("error in finding lyric-body",));
|
||||
};
|
||||
|
||||
|
|
@ -430,7 +379,8 @@ mod test {
|
|||
title: "Death Was Arrested".to_string(),
|
||||
author: "North Point Worship (Ft. Seth Condrey)".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(
|
||||
"Death was arrested".to_string(),
|
||||
|
|
@ -444,13 +394,10 @@ mod test {
|
|||
"There was no song that matched on Genius"
|
||||
);
|
||||
|
||||
let titles: Vec<String> =
|
||||
hits.iter().map(|song| song.title.clone()).collect();
|
||||
let titles: Vec<String> = hits.iter().map(|song| song.title.clone()).collect();
|
||||
dbg!(titles);
|
||||
for hit in hits {
|
||||
let new_song = get_genius_lyrics(hit)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let new_song = get_genius_lyrics(hit).await.map_err(|e| e.to_string())?;
|
||||
dbg!(&new_song);
|
||||
dbg!(&new_song.provider);
|
||||
if new_song.lyrics.starts_with("[Verse 1]") {
|
||||
|
|
@ -467,13 +414,12 @@ mod test {
|
|||
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
|
||||
// })
|
||||
map.keys().contains(&VerseName::Verse { number: 1 }) // && map.keys().contains(&VerseName::Verse {
|
||||
// number: 2
|
||||
// })
|
||||
// && map.keys().contains(&VerseName::Chorus {
|
||||
// number: 1
|
||||
// })
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
|
|
@ -502,9 +448,10 @@ mod test {
|
|||
let songs = lyrics_com_link_to_song(links)
|
||||
.await
|
||||
.map_err(|e| format!("{e}"))?;
|
||||
if let Some(first) = songs.iter().find_or_first(|song| {
|
||||
song.author == "North Point InsideOut"
|
||||
}) {
|
||||
if let Some(first) = songs
|
||||
.iter()
|
||||
.find_or_first(|song| song.author == "North Point InsideOut")
|
||||
{
|
||||
assert_eq!(&song, first);
|
||||
// online_song_to_song(song)?;
|
||||
}
|
||||
|
|
@ -516,14 +463,10 @@ mod test {
|
|||
let song = Song::from(song);
|
||||
if let Some(verse_map) = song.verse_map.as_ref() {
|
||||
if verse_map.is_empty() {
|
||||
return Err(format!(
|
||||
"VerseMap wasn't built right likely: {song:?}",
|
||||
));
|
||||
return Err(format!("VerseMap wasn't built right likely: {song:?}",));
|
||||
}
|
||||
} else {
|
||||
return Err(String::from(
|
||||
"There is no VerseMap in this song",
|
||||
));
|
||||
return Err(String::from("There is no VerseMap in this song"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ use crate::core::slide::{self, Background, TextAlignment};
|
|||
use crate::ui::text_svg::{Color, Font, Stroke, shadow, stroke};
|
||||
use crate::{Slide, SlideBuilder};
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Song {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
|
|
@ -54,16 +52,7 @@ pub struct Song {
|
|||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Hash,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord,
|
||||
)]
|
||||
pub enum VerseName {
|
||||
Verse { number: usize },
|
||||
|
|
@ -115,33 +104,15 @@ impl VerseName {
|
|||
#[must_use]
|
||||
pub fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::Verse { number } => {
|
||||
Self::Verse { number: number + 1 }
|
||||
}
|
||||
Self::PreChorus { number } => {
|
||||
Self::PreChorus { number: number + 1 }
|
||||
}
|
||||
Self::Chorus { number } => {
|
||||
Self::Chorus { number: number + 1 }
|
||||
}
|
||||
Self::PostChorus { number } => {
|
||||
Self::PostChorus { number: number + 1 }
|
||||
}
|
||||
Self::Bridge { number } => {
|
||||
Self::Bridge { number: number + 1 }
|
||||
}
|
||||
Self::Intro { number } => {
|
||||
Self::Intro { number: number + 1 }
|
||||
}
|
||||
Self::Outro { number } => {
|
||||
Self::Outro { number: number + 1 }
|
||||
}
|
||||
Self::Instrumental { number } => {
|
||||
Self::Instrumental { number: number + 1 }
|
||||
}
|
||||
Self::Other { number } => {
|
||||
Self::Other { number: number + 1 }
|
||||
}
|
||||
Self::Verse { number } => Self::Verse { number: number + 1 },
|
||||
Self::PreChorus { number } => Self::PreChorus { number: number + 1 },
|
||||
Self::Chorus { number } => Self::Chorus { number: number + 1 },
|
||||
Self::PostChorus { number } => Self::PostChorus { number: number + 1 },
|
||||
Self::Bridge { number } => Self::Bridge { number: number + 1 },
|
||||
Self::Intro { number } => Self::Intro { number: number + 1 },
|
||||
Self::Outro { number } => Self::Outro { number: number + 1 },
|
||||
Self::Instrumental { number } => Self::Instrumental { number: number + 1 },
|
||||
Self::Other { number } => Self::Other { number: number + 1 },
|
||||
Self::Blank => Self::Blank,
|
||||
}
|
||||
}
|
||||
|
|
@ -202,9 +173,7 @@ impl VerseName {
|
|||
impl TryFrom<(Vec<u8>, String)> for VerseName {
|
||||
type Error = miette::Error;
|
||||
|
||||
fn try_from(
|
||||
value: (Vec<u8>, String),
|
||||
) -> std::result::Result<Self, Self::Error> {
|
||||
fn try_from(value: (Vec<u8>, String)) -> std::result::Result<Self, Self::Error> {
|
||||
let (data, mime) = value;
|
||||
debug!(?mime);
|
||||
ron::de::from_bytes(&data).into_diagnostic()
|
||||
|
|
@ -216,10 +185,7 @@ impl AsMimeTypes for VerseName {
|
|||
Cow::from(vec!["application/verse".to_string()])
|
||||
}
|
||||
|
||||
fn as_bytes(
|
||||
&self,
|
||||
_mime_type: &str,
|
||||
) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||
fn as_bytes(&self, _mime_type: &str) -> Option<std::borrow::Cow<'static, [u8]>> {
|
||||
let ron = ron::ser::to_string(self).ok()?;
|
||||
Some(Cow::from(ron.into_bytes()))
|
||||
}
|
||||
|
|
@ -275,9 +241,7 @@ impl ServiceTrait for Song {
|
|||
let lyrics: Vec<String> = self
|
||||
.verses
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
miette!("There are no verses assigned yet.")
|
||||
})?
|
||||
.ok_or_else(|| miette!("There are no verses assigned yet."))?
|
||||
.iter()
|
||||
.filter_map(|verse| self.get_lyric(verse))
|
||||
.flat_map(|lyric| {
|
||||
|
|
@ -293,34 +257,21 @@ impl ServiceTrait for Song {
|
|||
.iter()
|
||||
.filter_map(|l| {
|
||||
let font = Font::default()
|
||||
.name(
|
||||
self.font
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Calibri".into()),
|
||||
)
|
||||
.name(self.font.clone().unwrap_or_else(|| "Calibri".into()))
|
||||
.style(self.font_style.unwrap_or_default())
|
||||
.weight(self.font_weight.unwrap_or_default())
|
||||
.size(
|
||||
u8::try_from(self.font_size.unwrap_or(100))
|
||||
.unwrap_or(100),
|
||||
);
|
||||
let stroke_size =
|
||||
self.stroke_size.unwrap_or_default();
|
||||
.size(u8::try_from(self.font_size.unwrap_or(100)).unwrap_or(100));
|
||||
let stroke_size = self.stroke_size.unwrap_or_default();
|
||||
let stroke: Stroke = stroke(
|
||||
stroke_size,
|
||||
self.stroke_color
|
||||
.map(Color::from)
|
||||
.unwrap_or_default(),
|
||||
self.stroke_color.map(Color::from).unwrap_or_default(),
|
||||
);
|
||||
let shadow_size =
|
||||
self.shadow_size.unwrap_or_default();
|
||||
let shadow_size = self.shadow_size.unwrap_or_default();
|
||||
let shadow = shadow(
|
||||
self.shadow_offset.unwrap_or_default().0,
|
||||
self.shadow_offset.unwrap_or_default().1,
|
||||
shadow_size,
|
||||
self.shadow_color
|
||||
.map(Color::from)
|
||||
.unwrap_or_default(),
|
||||
self.shadow_color.map(Color::from).unwrap_or_default(),
|
||||
);
|
||||
let builder = SlideBuilder::new();
|
||||
let builder = if shadow_size > 0 {
|
||||
|
|
@ -334,18 +285,12 @@ impl ServiceTrait for Song {
|
|||
builder
|
||||
};
|
||||
builder
|
||||
.background(
|
||||
self.background.clone().unwrap_or_default(),
|
||||
)
|
||||
.background(self.background.clone().unwrap_or_default())
|
||||
.font(font)
|
||||
.font_size(self.font_size.unwrap_or_default())
|
||||
.text_alignment(
|
||||
self.text_alignment.unwrap_or_default(),
|
||||
)
|
||||
.text_alignment(self.text_alignment.unwrap_or_default())
|
||||
.text_color(
|
||||
self.text_color.unwrap_or_else(|| {
|
||||
Srgb::new(1.0, 1.0, 1.0)
|
||||
}),
|
||||
self.text_color.unwrap_or_else(|| Srgb::new(1.0, 1.0, 1.0)),
|
||||
)
|
||||
.audio(self.audio.clone().unwrap_or_default())
|
||||
.video_loop(true)
|
||||
|
|
@ -370,27 +315,19 @@ impl FromRow<'_, SqliteRow> for Song {
|
|||
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||
let lyrics: &str = row.try_get("lyrics")?;
|
||||
|
||||
let Ok(verse_map) = ron::de::from_str::<
|
||||
Option<HashMap<VerseName, String>>,
|
||||
>(lyrics) else {
|
||||
let Ok(verse_map) =
|
||||
ron::de::from_str::<Option<HashMap<VerseName, String>>>(lyrics)
|
||||
else {
|
||||
return Err(sqlx::Error::ColumnDecode {
|
||||
index: "8".into(),
|
||||
source: miette!(
|
||||
"Couldn't decode the song into verses"
|
||||
)
|
||||
.into(),
|
||||
source: miette!("Couldn't decode the song into verses").into(),
|
||||
});
|
||||
};
|
||||
let verse_order: &str = row.try_get("verse_order")?;
|
||||
let Ok(verses) =
|
||||
ron::de::from_str::<Option<Vec<VerseName>>>(verse_order)
|
||||
else {
|
||||
let Ok(verses) = ron::de::from_str::<Option<Vec<VerseName>>>(verse_order) else {
|
||||
return Err(sqlx::Error::ColumnDecode {
|
||||
index: "0".into(),
|
||||
source: miette!(
|
||||
"Couldn't decode the song into verses"
|
||||
)
|
||||
.into(),
|
||||
source: miette!("Couldn't decode the song into verses").into(),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -411,17 +348,13 @@ impl FromRow<'_, SqliteRow> for Song {
|
|||
let stroke_color = row
|
||||
.try_get("stroke_color")
|
||||
.ok()
|
||||
.and_then(|color: String| {
|
||||
ron::de::from_str::<Option<Srgb>>(&color).ok()
|
||||
})
|
||||
.and_then(|color: String| ron::de::from_str::<Option<Srgb>>(&color).ok())
|
||||
.flatten();
|
||||
let shadow_size = row.try_get("shadow_size").ok();
|
||||
let shadow_color = row
|
||||
.try_get("shadow_color")
|
||||
.ok()
|
||||
.and_then(|color: String| {
|
||||
ron::de::from_str::<Option<Srgb>>(&color).ok()
|
||||
})
|
||||
.and_then(|color: String| ron::de::from_str::<Option<Srgb>>(&color).ok())
|
||||
.flatten();
|
||||
let shadow_offset = match (
|
||||
row.try_get("shadow_offset_x").ok(),
|
||||
|
|
@ -432,16 +365,14 @@ impl FromRow<'_, SqliteRow> for Song {
|
|||
};
|
||||
|
||||
let style_string: String = row.try_get("style")?;
|
||||
let font_style =
|
||||
ron::de::from_str::<Option<Style>>(&style_string)
|
||||
.ok()
|
||||
.flatten();
|
||||
let font_style = ron::de::from_str::<Option<Style>>(&style_string)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let weight_string: String = row.try_get("weight")?;
|
||||
let font_weight =
|
||||
ron::de::from_str::<Option<Weight>>(&weight_string)
|
||||
.ok()
|
||||
.flatten();
|
||||
let font_weight = ron::de::from_str::<Option<Weight>>(&weight_string)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let lyric_video = row
|
||||
.try_get::<String, &str>("lyric_video")
|
||||
|
|
@ -480,12 +411,8 @@ impl FromRow<'_, SqliteRow> for Song {
|
|||
("left", "center") => TextAlignment::MiddleLeft,
|
||||
("left", "bottom") => TextAlignment::BottomLeft,
|
||||
("center", "top") => TextAlignment::TopCenter,
|
||||
("center", "center") => {
|
||||
TextAlignment::MiddleCenter
|
||||
}
|
||||
("center", "bottom") => {
|
||||
TextAlignment::BottomCenter
|
||||
}
|
||||
("center", "center") => TextAlignment::MiddleCenter,
|
||||
("center", "bottom") => TextAlignment::BottomCenter,
|
||||
("right", "top") => TextAlignment::TopRight,
|
||||
("right", "center") => TextAlignment::MiddleRight,
|
||||
("right", "bottom") => TextAlignment::BottomRight,
|
||||
|
|
@ -536,10 +463,10 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
|||
DEFAULT_SONG_ID
|
||||
};
|
||||
|
||||
let background = if let Some(key_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("background"))
|
||||
}) {
|
||||
let background = if let Some(key_pos) = list
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("background")))
|
||||
{
|
||||
let pos = key_pos + 1;
|
||||
list.get(pos).map(slide::lisp_to_background)
|
||||
} else {
|
||||
|
|
@ -586,9 +513,10 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
|||
None
|
||||
};
|
||||
|
||||
let font_size = if let Some(key_pos) = list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("font-size"))
|
||||
}) {
|
||||
let font_size = if let Some(key_pos) = list
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("font-size")))
|
||||
{
|
||||
let pos = key_pos + 1;
|
||||
list.get(pos).map(i32::from)
|
||||
} else {
|
||||
|
|
@ -606,20 +534,20 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
|||
String::from("song")
|
||||
};
|
||||
|
||||
let text_alignment = if let Some(key_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("text-alignment"))
|
||||
}) {
|
||||
let text_alignment = if let Some(key_pos) = list
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("text-alignment")))
|
||||
{
|
||||
let pos = key_pos + 1;
|
||||
list.get(pos).map(TextAlignment::from)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let verse_order = if let Some(key_pos) =
|
||||
list.iter().position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("verse-order"))
|
||||
}) {
|
||||
let verse_order = if let Some(key_pos) = list
|
||||
.iter()
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("verse-order")))
|
||||
{
|
||||
let pos = key_pos + 1;
|
||||
list.get(pos).map(|v| match v {
|
||||
Value::List(vals) => vals
|
||||
|
|
@ -693,8 +621,7 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
|||
lyrics.push(lyric);
|
||||
}
|
||||
|
||||
let lyrics: String =
|
||||
lyrics.iter().flat_map(|s| s.chars()).collect();
|
||||
let lyrics: String = lyrics.iter().flat_map(|s| s.chars()).collect();
|
||||
let lyrics = lyrics.trim_start().to_string();
|
||||
|
||||
Song {
|
||||
|
|
@ -713,10 +640,7 @@ pub fn lisp_to_song(list: Vec<Value>) -> Song {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn get_song_from_db(
|
||||
id: i32,
|
||||
db: Arc<SqlitePool>,
|
||||
) -> Result<Song> {
|
||||
pub async fn get_song_from_db(id: i32, db: Arc<SqlitePool>) -> Result<Song> {
|
||||
let row = query("SELECT verse_order, font_size, background_type, horizontal_text_alignment, vertical_text_alignment, title, font, background, lyrics, ccli, author, audio, stroke_size, stroke_color, shadow_color, shadow_size, shadow_offset_x, shadow_offset_y, style, weight, id from songs where id = $1").bind(id).fetch_one(&*db).await.into_diagnostic()?;
|
||||
Song::from_row(&row).into_diagnostic()
|
||||
}
|
||||
|
|
@ -786,9 +710,7 @@ pub async fn remove_song(
|
|||
mut songs: Vec<Song>,
|
||||
id: i32,
|
||||
) -> Result<Vec<Song>> {
|
||||
if let Some(index) =
|
||||
songs.iter().position(|current_song| current_song.id == id)
|
||||
{
|
||||
if let Some(index) = songs.iter().position(|current_song| current_song.id == id) {
|
||||
songs.remove(index);
|
||||
query!("DELETE FROM songs WHERE id = $1", id)
|
||||
.execute(&*db)
|
||||
|
|
@ -853,17 +775,13 @@ pub async fn insert_song(
|
|||
.execute(&*db)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
song.id = i32::try_from(res.last_insert_rowid()).expect(
|
||||
"Fairly confident that this number won't get that high",
|
||||
);
|
||||
song.id = i32::try_from(res.last_insert_rowid())
|
||||
.expect("Fairly confident that this number won't get that high");
|
||||
songs.push(song);
|
||||
Ok(songs)
|
||||
}
|
||||
|
||||
pub async fn add_song(
|
||||
songs: Vec<Song>,
|
||||
db: Arc<SqlitePool>,
|
||||
) -> Result<Vec<Song>> {
|
||||
pub async fn add_song(songs: Vec<Song>, db: Arc<SqlitePool>) -> Result<Vec<Song>> {
|
||||
let song = Song::default();
|
||||
insert_song(song, songs, db).await
|
||||
}
|
||||
|
|
@ -876,8 +794,7 @@ pub async fn update_song(
|
|||
// self.update_item(item.clone(), index)?;
|
||||
|
||||
// debug!(?item);
|
||||
let verse_order =
|
||||
ron::ser::to_string(&song.verses).into_diagnostic()?;
|
||||
let verse_order = ron::ser::to_string(&song.verses).into_diagnostic()?;
|
||||
|
||||
let audio = song
|
||||
.audio
|
||||
|
|
@ -909,36 +826,30 @@ pub async fn update_song(
|
|||
});
|
||||
let lyrics = ron::ser::to_string(&lyrics).into_diagnostic()?;
|
||||
|
||||
let (vertical_alignment, horizontal_alignment) =
|
||||
song.text_alignment.map_or_else(
|
||||
|| ("center", "center"),
|
||||
|ta| match ta {
|
||||
TextAlignment::TopLeft => ("top", "left"),
|
||||
TextAlignment::TopCenter => ("top", "center"),
|
||||
TextAlignment::TopRight => ("top", "right"),
|
||||
TextAlignment::MiddleLeft => ("center", "left"),
|
||||
TextAlignment::MiddleCenter => ("center", "center"),
|
||||
TextAlignment::MiddleRight => ("center", "right"),
|
||||
TextAlignment::BottomLeft => ("bottom", "left"),
|
||||
TextAlignment::BottomCenter => ("bottom", "center"),
|
||||
TextAlignment::BottomRight => ("bottom", "right"),
|
||||
},
|
||||
);
|
||||
let (vertical_alignment, horizontal_alignment) = song.text_alignment.map_or_else(
|
||||
|| ("center", "center"),
|
||||
|ta| match ta {
|
||||
TextAlignment::TopLeft => ("top", "left"),
|
||||
TextAlignment::TopCenter => ("top", "center"),
|
||||
TextAlignment::TopRight => ("top", "right"),
|
||||
TextAlignment::MiddleLeft => ("center", "left"),
|
||||
TextAlignment::MiddleCenter => ("center", "center"),
|
||||
TextAlignment::MiddleRight => ("center", "right"),
|
||||
TextAlignment::BottomLeft => ("bottom", "left"),
|
||||
TextAlignment::BottomCenter => ("bottom", "center"),
|
||||
TextAlignment::BottomRight => ("bottom", "right"),
|
||||
},
|
||||
);
|
||||
|
||||
let stroke_size = song.stroke_size.unwrap_or_default();
|
||||
let shadow_size = song.shadow_size.unwrap_or_default();
|
||||
let (shadow_offset_x, shadow_offset_y) =
|
||||
song.shadow_offset.unwrap_or_default();
|
||||
let (shadow_offset_x, shadow_offset_y) = song.shadow_offset.unwrap_or_default();
|
||||
|
||||
let stroke_color =
|
||||
ron::ser::to_string(&song.stroke_color).into_diagnostic()?;
|
||||
let shadow_color =
|
||||
ron::ser::to_string(&song.shadow_color).into_diagnostic()?;
|
||||
let stroke_color = ron::ser::to_string(&song.stroke_color).into_diagnostic()?;
|
||||
let shadow_color = ron::ser::to_string(&song.shadow_color).into_diagnostic()?;
|
||||
|
||||
let style =
|
||||
ron::ser::to_string(&song.font_style).into_diagnostic()?;
|
||||
let weight =
|
||||
ron::ser::to_string(&song.font_weight).into_diagnostic()?;
|
||||
let style = ron::ser::to_string(&song.font_style).into_diagnostic()?;
|
||||
let weight = ron::ser::to_string(&song.font_weight).into_diagnostic()?;
|
||||
|
||||
// debug!(
|
||||
// ?stroke_size,
|
||||
|
|
@ -997,17 +908,14 @@ impl Song {
|
|||
#[must_use]
|
||||
pub fn get_lyric(&self, verse: &VerseName) -> Option<String> {
|
||||
self.verse_map.as_ref().and_then(|verse_map| {
|
||||
verse_map.get(verse).cloned().map(|lyric| {
|
||||
lyric.trim().trim_end_matches('\n').to_string()
|
||||
})
|
||||
verse_map
|
||||
.get(verse)
|
||||
.cloned()
|
||||
.map(|lyric| lyric.trim().trim_end_matches('\n').to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_lyrics<T: Into<String>>(
|
||||
&mut self,
|
||||
verse: &VerseName,
|
||||
lyrics: T,
|
||||
) {
|
||||
pub fn set_lyrics<T: Into<String>>(&mut self, verse: &VerseName, lyrics: T) {
|
||||
let lyric_copy = lyrics.into().trim().to_string();
|
||||
if let Some(verse_map) = self.verse_map.as_mut() {
|
||||
// debug!(?verse_map, "should update");
|
||||
|
|
@ -1044,11 +952,7 @@ impl Song {
|
|||
Err(miette!("No verses in this song yet"))
|
||||
}
|
||||
|
||||
pub fn update_verse_name(
|
||||
&mut self,
|
||||
verse: VerseName,
|
||||
old_verse: &VerseName,
|
||||
) {
|
||||
pub fn update_verse_name(&mut self, verse: VerseName, old_verse: &VerseName) {
|
||||
if let Some(verse_map) = self.verse_map.as_mut()
|
||||
&& let Some(lyric) = verse_map.remove(old_verse)
|
||||
{
|
||||
|
|
@ -1073,12 +977,7 @@ impl Song {
|
|||
// the song can be sent to the db and it's lyrics will actually change. Or we
|
||||
// could have the update_song_in_db function recreate the lyrics from the new
|
||||
// verse layout. But I do feel like it belongs here more.
|
||||
pub fn update_verse(
|
||||
&mut self,
|
||||
index: usize,
|
||||
verse: VerseName,
|
||||
lyric: String,
|
||||
) {
|
||||
pub fn update_verse(&mut self, index: usize, verse: VerseName, lyric: String) {
|
||||
debug!(index, ?verse, lyric);
|
||||
self.set_lyrics(&verse, lyric);
|
||||
if let Some(verses) = self.verses.as_mut()
|
||||
|
|
@ -1148,25 +1047,21 @@ impl Song {
|
|||
if let Some(verse_names) = &self.verses {
|
||||
let verses = verse_names
|
||||
.iter()
|
||||
.filter(|verse| {
|
||||
matches!(verse, VerseName::Verse { .. })
|
||||
})
|
||||
.filter(|verse| matches!(verse, VerseName::Verse { .. }))
|
||||
.sorted();
|
||||
let mut choruses = verse_names.iter().filter(|verse| {
|
||||
matches!(verse, VerseName::Chorus { .. })
|
||||
});
|
||||
let mut bridges = verse_names.iter().filter(|verse| {
|
||||
matches!(verse, VerseName::Bridge { .. })
|
||||
});
|
||||
let mut choruses = verse_names
|
||||
.iter()
|
||||
.filter(|verse| matches!(verse, VerseName::Chorus { .. }));
|
||||
let mut bridges = verse_names
|
||||
.iter()
|
||||
.filter(|verse| matches!(verse, VerseName::Bridge { .. }));
|
||||
if verses.len() == 0 {
|
||||
VerseName::Verse { number: 1 }
|
||||
} else if choruses.next().is_none() {
|
||||
VerseName::Chorus { number: 1 }
|
||||
} else if verses.len() == 1 {
|
||||
let verse_number =
|
||||
if let Some(VerseName::Verse { number }) =
|
||||
verses.last()
|
||||
{
|
||||
if let Some(VerseName::Verse { number }) = verses.last() {
|
||||
*number
|
||||
} else {
|
||||
0
|
||||
|
|
@ -1190,11 +1085,7 @@ impl Song {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_verse(
|
||||
&mut self,
|
||||
verse: VerseName,
|
||||
lyric: impl Into<String>,
|
||||
) {
|
||||
pub fn add_verse(&mut self, verse: VerseName, lyric: impl Into<String>) {
|
||||
let lyric: String = lyric.into();
|
||||
self.set_lyrics(&verse, lyric);
|
||||
if let Some(verses) = self.verses.as_mut() {
|
||||
|
|
@ -1348,11 +1239,10 @@ You saved my soul"
|
|||
VerseName::Outro { number: 1 },
|
||||
VerseName::Blank,
|
||||
]);
|
||||
song.verse_order =
|
||||
"O1 V1 C1 C2 O2 V2 C3 C2 O2 B1 C2 C2 E1 O2"
|
||||
.split(' ')
|
||||
.map(|s| Some(s.to_string()))
|
||||
.collect();
|
||||
song.verse_order = "O1 V1 C1 C2 O2 V2 C3 C2 O2 B1 C2 C2 E1 O2"
|
||||
.split(' ')
|
||||
.map(|s| Some(s.to_string()))
|
||||
.collect();
|
||||
let lyrics = song.get_lyrics();
|
||||
match lyrics {
|
||||
Ok(lyrics) => {
|
||||
|
|
@ -1394,8 +1284,7 @@ You saved my soul"
|
|||
|
||||
pub async fn add_db() -> Result<SqlitePool> {
|
||||
let db_url = String::from("sqlite::memory:");
|
||||
let pool =
|
||||
SqlitePool::connect(&db_url).await.into_diagnostic()?;
|
||||
let pool = SqlitePool::connect(&db_url).await.into_diagnostic()?;
|
||||
migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
|
|
@ -1426,8 +1315,7 @@ You saved my soul"
|
|||
let song_model = Model::new_song_model(Arc::clone(&db)).await;
|
||||
let length = song_model.items.len();
|
||||
assert!(song_model.items.len() == 20, "Length is {length}");
|
||||
let ids: Vec<i32> =
|
||||
song_model.items.iter().map(|s| s.id).collect();
|
||||
let ids: Vec<i32> = song_model.items.iter().map(|s| s.id).collect();
|
||||
if let Some(song) = song_model.find(|s| s.id == 7) {
|
||||
let test_song = test_song();
|
||||
if let Ok(song_lyrics) = song.get_lyrics()
|
||||
|
|
@ -1439,9 +1327,7 @@ You saved my soul"
|
|||
}
|
||||
} else {
|
||||
dbg!(song_model);
|
||||
panic!(
|
||||
"Failed to find song in model: Id's of all songs are {ids:?}"
|
||||
);
|
||||
panic!("Failed to find song in model: Id's of all songs are {ids:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1490,9 +1376,7 @@ You saved my soul"
|
|||
assert_ne!(test_song, Some(&cloned_song));
|
||||
|
||||
let songs = song_model.items.clone();
|
||||
match update_song(cloned_song.clone(), songs, Arc::clone(&db))
|
||||
.await
|
||||
{
|
||||
match update_song(cloned_song.clone(), songs, Arc::clone(&db)).await {
|
||||
Ok(_) => {
|
||||
let db_song = get_song_from_db(7, db)
|
||||
.await
|
||||
|
|
@ -1501,10 +1385,7 @@ You saved my soul"
|
|||
// lyrics will be in the wrong order since serialization
|
||||
// can sometimes change the order of the verse_map
|
||||
assert_eq!(db_song.id, cloned_song.id);
|
||||
assert_eq!(
|
||||
db_song.verse_order,
|
||||
cloned_song.verse_order
|
||||
);
|
||||
assert_eq!(db_song.verse_order, cloned_song.verse_order);
|
||||
assert_eq!(db_song.verse_map, cloned_song.verse_map);
|
||||
}
|
||||
Err(e) => panic!("{e}"),
|
||||
|
|
@ -1515,8 +1396,7 @@ You saved my soul"
|
|||
pub fn test_song() -> Song {
|
||||
let lyrics = "Some({Verse(number:4):\"Our Savior displayed\\nOn a criminal\\'s cross\\n\\nDarkness rejoiced as though\\nHeaven had lost\\n\\nBut then Jesus arose\\nWith our freedom in hand\\n\\nThat\\'s when death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Intro(number:1):\"Death Was Arrested\\nNorth Point Worship\",Verse(number:3):\"Released from my chains,\\nI\\'m a prisoner no more\\n\\nMy shame was a ransom\\nHe faithfully bore\\n\\nHe cancelled my debt and\\nHe called me His friend\\n\\nWhen death was arrested\\nAnd my life began\",Bridge(number:1):\"Oh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\\n\\nOh, we\\'re free, free,\\nForever we\\'re free\\n\\nCome join the song\\nOf all the redeemed\\n\\nYes, we\\'re free, free,\\nForever amen\\n\\nWhen death was arrested\\nAnd my life began\",Other(number:99):\"When death was arrested\\nAnd my life began\\n\\nThat\\'s when death was arrested\\nAnd my life began\",Verse(number:2):\"Ash was redeemed\\nOnly beauty remains\\n\\nMy orphan heart\\nWas given a name\\n\\nMy mourning grew quiet,\\nMy feet rose to dance\\n\\nWhen death was arrested\\nAnd my life began\",Verse(number:1):\"Alone in my sorrow\\nAnd dead in my sin\\n\\nLost without hope\\nWith no place to begin\\n\\nYour love made a way\\nTo let mercy come in\\n\\nWhen death was arrested\\nAnd my life began\",Chorus(number:1):\"Oh, Your grace so free,\\nWashes over me\\n\\nYou have made me new,\\nNow life begins with You\\n\\nIt\\'s Your endless love,\\nPouring down on us\\n\\nYou have made us new,\\nNow life begins with You\"})".to_string();
|
||||
let verse_map: Option<HashMap<VerseName, String>> =
|
||||
ron::from_str(&lyrics)
|
||||
.expect("Error creating lyrics object from string");
|
||||
ron::from_str(&lyrics).expect("Error creating lyrics object from string");
|
||||
Song {
|
||||
id: 7,
|
||||
title: "Death Was Arrested".to_string(),
|
||||
|
|
@ -1604,19 +1484,14 @@ You saved my soul"
|
|||
.collect();
|
||||
let fontdb = Arc::new(fontdb::Database::new());
|
||||
songs.into_par_iter().for_each(|song| {
|
||||
let slides = song
|
||||
.to_slides()
|
||||
.expect("Error in making slides from song");
|
||||
let slides = song.to_slides().expect("Error in making slides from song");
|
||||
slides.into_par_iter().for_each(|slide| {
|
||||
text_svg_generator_with_cache(slide, &fontdb, None)
|
||||
.map_or_else(
|
||||
|e| panic!("{e}"),
|
||||
|slide| {
|
||||
assert!(slide.text_svg.is_some_and(
|
||||
|svg| svg.handle.is_some()
|
||||
));
|
||||
},
|
||||
);
|
||||
text_svg_generator_with_cache(slide, &fontdb, None).map_or_else(
|
||||
|e| panic!("{e}"),
|
||||
|slide| {
|
||||
assert!(slide.text_svg.is_some_and(|svg| svg.handle.is_some()));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ use std::process::Command;
|
|||
use std::{fs, str};
|
||||
use tracing::debug;
|
||||
|
||||
pub fn bg_from_video(
|
||||
video: &Path,
|
||||
screenshot: &Path,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
pub fn bg_from_video(video: &Path, screenshot: &Path) -> Result<(), Box<dyn Error>> {
|
||||
if screenshot.exists() {
|
||||
debug!("Screenshot already exists");
|
||||
} else {
|
||||
|
|
@ -39,10 +36,8 @@ pub fn bg_from_video(
|
|||
}
|
||||
}
|
||||
let hours: i32 = hours.parse().unwrap_or_default();
|
||||
let mut minutes: i32 =
|
||||
minutes.parse().unwrap_or_default();
|
||||
let mut seconds: i32 =
|
||||
seconds.parse().unwrap_or_default();
|
||||
let mut minutes: i32 = minutes.parse().unwrap_or_default();
|
||||
let mut seconds: i32 = seconds.parse().unwrap_or_default();
|
||||
minutes += hours * 60;
|
||||
seconds += minutes * 60;
|
||||
at_second = seconds / 5;
|
||||
|
|
@ -70,18 +65,15 @@ pub fn bg_from_video(
|
|||
pub fn bg_path_from_video(video: &Path) -> PathBuf {
|
||||
let video = PathBuf::from(video);
|
||||
debug!(?video);
|
||||
let mut data_dir =
|
||||
dirs::cache_dir().expect("Can't find cache dir");
|
||||
let mut data_dir = dirs::cache_dir().expect("Can't find cache dir");
|
||||
data_dir.push("lumina");
|
||||
data_dir.push("thumbnails");
|
||||
let _ = fs::create_dir_all(&data_dir);
|
||||
if !data_dir.exists() {
|
||||
fs::create_dir(&data_dir)
|
||||
.expect("Could not create thumbnails dir");
|
||||
fs::create_dir(&data_dir).expect("Could not create thumbnails dir");
|
||||
}
|
||||
let mut screenshot = data_dir.clone();
|
||||
screenshot
|
||||
.push(video.file_name().expect("Should have file name"));
|
||||
screenshot.push(video.file_name().expect("Should have file name"));
|
||||
screenshot.set_extension("png");
|
||||
screenshot
|
||||
}
|
||||
|
|
@ -96,10 +88,9 @@ mod test {
|
|||
let screenshot = bg_path_from_video(video);
|
||||
match bg_from_video(video, &screenshot) {
|
||||
Ok(_o) => assert!(screenshot.exists()),
|
||||
Err(e) => debug_assert!(
|
||||
false,
|
||||
"There was an error in the runtime future. {e}",
|
||||
),
|
||||
Err(e) => {
|
||||
debug_assert!(false, "There was an error in the runtime future. {e}",)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Video {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
|
|
@ -95,30 +93,21 @@ impl From<&Value> for Video {
|
|||
Value::List(list) => {
|
||||
let path = list
|
||||
.iter()
|
||||
.position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("source"))
|
||||
})
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("source")))
|
||||
.and_then(|path_pos| {
|
||||
let pos = path_pos + 1;
|
||||
list.get(pos)
|
||||
.map(|p| PathBuf::from(String::from(p)))
|
||||
list.get(pos).map(|p| PathBuf::from(String::from(p)))
|
||||
});
|
||||
|
||||
let title = path.clone().map(|p| {
|
||||
let path =
|
||||
p.to_str().unwrap_or_default().to_string();
|
||||
let title =
|
||||
path.rsplit_once('/').unwrap_or_default().1;
|
||||
let path = p.to_str().unwrap_or_default().to_string();
|
||||
let title = path.rsplit_once('/').unwrap_or_default().1;
|
||||
title.to_string()
|
||||
});
|
||||
|
||||
let start_time = list
|
||||
.iter()
|
||||
.position(|v| {
|
||||
v == &Value::Keyword(Keyword::from(
|
||||
"start-time",
|
||||
))
|
||||
})
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("start-time")))
|
||||
.and_then(|start_pos| {
|
||||
let pos = start_pos + 1;
|
||||
list.get(pos).map(|p| i32::from(p) as f32)
|
||||
|
|
@ -126,11 +115,7 @@ impl From<&Value> for Video {
|
|||
|
||||
let end_time = list
|
||||
.iter()
|
||||
.position(|v| {
|
||||
v == &Value::Keyword(Keyword::from(
|
||||
"end-time",
|
||||
))
|
||||
})
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("end-time")))
|
||||
.and_then(|end_pos| {
|
||||
let pos = end_pos + 1;
|
||||
list.get(pos).map(|p| i32::from(p) as f32)
|
||||
|
|
@ -138,14 +123,10 @@ impl From<&Value> for Video {
|
|||
|
||||
let looping = list
|
||||
.iter()
|
||||
.position(|v| {
|
||||
v == &Value::Keyword(Keyword::from("loop"))
|
||||
})
|
||||
.position(|v| v == &Value::Keyword(Keyword::from("loop")))
|
||||
.is_some_and(|loop_pos| {
|
||||
let pos = loop_pos + 1;
|
||||
list.get(pos).is_some_and(|l| {
|
||||
String::from(l) == *"true"
|
||||
})
|
||||
list.get(pos).is_some_and(|l| String::from(l) == *"true")
|
||||
});
|
||||
|
||||
Self {
|
||||
|
|
@ -173,10 +154,7 @@ impl ServiceTrait for Video {
|
|||
|
||||
fn to_slides(&self) -> Result<Vec<Slide>> {
|
||||
let slide = SlideBuilder::new()
|
||||
.background(
|
||||
Background::try_from(self.path.clone())
|
||||
.into_diagnostic()?,
|
||||
)
|
||||
.background(Background::try_from(self.path.clone()).into_diagnostic()?)
|
||||
.text("")
|
||||
.audio("")
|
||||
.font("")
|
||||
|
|
@ -214,9 +192,7 @@ impl Model<Video> {
|
|||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"There was an error in converting videos: {e}"
|
||||
);
|
||||
error!("There was an error in converting videos: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -329,10 +305,7 @@ pub async fn update_video(
|
|||
Ok(videos)
|
||||
}
|
||||
|
||||
pub async fn get_from_db(
|
||||
database_id: i32,
|
||||
db: &mut SqliteConnection,
|
||||
) -> Result<Video> {
|
||||
pub async fn get_from_db(database_id: i32, 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()
|
||||
}
|
||||
|
||||
|
|
@ -378,14 +351,8 @@ mod test {
|
|||
let new_video = test_video("A newer video".into());
|
||||
match result {
|
||||
Ok(()) => {
|
||||
assert_eq!(
|
||||
&video,
|
||||
video_model.find(|v| v.id == 0).expect("")
|
||||
);
|
||||
assert_ne!(
|
||||
&new_video,
|
||||
video_model.find(|v| v.id == 0).expect("")
|
||||
);
|
||||
assert_eq!(&video, video_model.find(|v| v.id == 0).expect(""));
|
||||
assert_ne!(&new_video, video_model.find(|v| v.id == 0).expect(""));
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("There was an error adding the video: {e}",)
|
||||
|
|
|
|||
1561
src/main.rs
1561
src/main.rs
File diff suppressed because it is too large
Load diff
|
|
@ -1,61 +1,146 @@
|
|||
// use iced_video_player::Video;
|
||||
use std::fmt::Display;
|
||||
use std::num::NonZero;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
// fn video_player(video: &Video) -> Element<Message> {}
|
||||
|
||||
use iced_video_player::Video;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use cosmic::widget::image::Handle;
|
||||
use iced_video_player::{Position, Video};
|
||||
use image::{DynamicImage, ImageFormat, RgbaImage};
|
||||
use url::Url;
|
||||
|
||||
pub fn create_video(url: &Url, framerate: u16) -> Result<Video> {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VideoSettings {
|
||||
pub mute: bool,
|
||||
pub framerate: u16,
|
||||
}
|
||||
|
||||
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
|
||||
use gstreamer_app::prelude::*;
|
||||
use {gstreamer as gst, gstreamer_app as gst_app};
|
||||
|
||||
gst::init().into_diagnostic()?;
|
||||
gst::init().map_err(VideoError::GlibError)?;
|
||||
|
||||
let pipeline = format!(
|
||||
r#"playbin uri="{}" video-sink="videoscale ! videoconvert ! videoflip method=automatic ! videorate ! appsink name=lumina_video drop=true caps=video/x-raw,format=NV12,framerate={framerate}/1,pixel-aspect-ratio=1/1""#,
|
||||
url.as_str()
|
||||
r#"playbin uri="{0}" video-sink="videoscale ! videoconvert ! videoflip method=automatic ! videorate ! appsink name=lumina_video drop=true caps=video/x-raw,format=NV12,framerate={1}/1,pixel-aspect-ratio=1/1{2}""#,
|
||||
url.as_str(),
|
||||
settings.framerate,
|
||||
if settings.mute { " mute=true" } else { "" },
|
||||
);
|
||||
|
||||
let pipeline =
|
||||
gst::parse::launch(pipeline.as_ref()).into_diagnostic()?;
|
||||
gst::parse::launch(pipeline.as_ref()).map_err(VideoError::GlibError)?;
|
||||
let pipeline = pipeline
|
||||
.downcast::<gst::Pipeline>()
|
||||
.map_err(|_| iced_video_player::Error::Cast)
|
||||
.into_diagnostic()?;
|
||||
.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(|_| iced_video_player::Error::Cast)
|
||||
.into_diagnostic()?;
|
||||
.map_err(|_| VideoError::IcedVideoError(iced_video_player::Error::Cast))?;
|
||||
let bin = pad
|
||||
.parent_element()
|
||||
.ok_or_else(|| {
|
||||
iced_video_player::Error::AppSink(String::from(
|
||||
VideoError::IcedVideoError(iced_video_player::Error::AppSink(String::from(
|
||||
"Should have a parent element here",
|
||||
))
|
||||
})
|
||||
.into_diagnostic()?
|
||||
)))
|
||||
})?
|
||||
.downcast::<gst::Bin>()
|
||||
.map_err(|_| iced_video_player::Error::Cast)
|
||||
.into_diagnostic()?;
|
||||
let video_sink = bin
|
||||
.by_name("lumina_video")
|
||||
.ok_or_else(|| {
|
||||
iced_video_player::Error::AppSink(String::from(
|
||||
"Can't find element lumina_video",
|
||||
))
|
||||
})
|
||||
.into_diagnostic()?;
|
||||
.map_err(|_| VideoError::IcedVideoError(iced_video_player::Error::Cast))?;
|
||||
let video_sink = bin.by_name("lumina_video").ok_or_else(|| {
|
||||
VideoError::IcedVideoError(iced_video_player::Error::AppSink(String::from(
|
||||
"Can't find element lumina_video",
|
||||
)))
|
||||
})?;
|
||||
let video_sink = video_sink
|
||||
.downcast::<gst_app::AppSink>()
|
||||
.map_err(|_| iced_video_player::Error::Cast)
|
||||
.into_diagnostic()?;
|
||||
let result = Video::from_gst_pipeline(pipeline, video_sink, None);
|
||||
result.into_diagnostic()
|
||||
.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: &Path) -> Result<Handle> {
|
||||
let thumbnails = {
|
||||
let mut video = create_video(
|
||||
input,
|
||||
&VideoSettings {
|
||||
mute: true,
|
||||
..Default::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"))
|
||||
})?;
|
||||
|
||||
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(gstreamer::glib::Error),
|
||||
ThumbnailImageError(image::ImageError),
|
||||
}
|
||||
|
||||
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, "GlipError: {error}")
|
||||
}
|
||||
Self::ThumbnailImageError(error) => {
|
||||
write!(f, "ImageError: {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ use cosmic::iced::Length;
|
|||
use cosmic::iced::alignment::Vertical;
|
||||
use cosmic::iced::widget::{column, row};
|
||||
use cosmic::widget::space::horizontal;
|
||||
use cosmic::widget::{
|
||||
self, Space, button, container, icon, text, text_input,
|
||||
};
|
||||
use cosmic::widget::{self, Space, button, container, icon, text, text_input};
|
||||
use cosmic::{Apply, Element, Task, theme};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
|
|
@ -69,21 +67,14 @@ impl ImageEditor {
|
|||
return Action::UpdateImage(image);
|
||||
}
|
||||
Message::PickImage => {
|
||||
let image_id = self
|
||||
.image
|
||||
.as_ref()
|
||||
.map(|v| v.id)
|
||||
.unwrap_or_default();
|
||||
let task = Task::perform(
|
||||
pick_image(),
|
||||
move |image_result| {
|
||||
image_result.map_or(Message::None, |image| {
|
||||
let mut image = Image::from(image);
|
||||
image.id = image_id;
|
||||
Message::Update(image)
|
||||
})
|
||||
},
|
||||
);
|
||||
let image_id = self.image.as_ref().map(|v| v.id).unwrap_or_default();
|
||||
let task = Task::perform(pick_image(), move |image_result| {
|
||||
image_result.map_or(Message::None, |image| {
|
||||
let mut image = Image::from(image);
|
||||
image.id = image_id;
|
||||
Message::Update(image)
|
||||
})
|
||||
});
|
||||
return Action::Task(task);
|
||||
}
|
||||
Message::None => (),
|
||||
|
|
@ -97,25 +88,21 @@ impl ImageEditor {
|
|||
|| Space::new().apply(container),
|
||||
|pic| widget::image(pic.path.clone()).apply(container),
|
||||
);
|
||||
let column = column![
|
||||
self.toolbar(),
|
||||
container.center_x(Length::FillPortion(2))
|
||||
]
|
||||
.spacing(theme::active().cosmic().space_l());
|
||||
let column = column![self.toolbar(), container.center_x(Length::FillPortion(2))]
|
||||
.spacing(theme::active().cosmic().space_l());
|
||||
column.into()
|
||||
}
|
||||
|
||||
fn toolbar(&self) -> Element<Message> {
|
||||
let title_box = text_input("Title...", &self.title)
|
||||
.on_input(Message::ChangeTitle);
|
||||
let title_box =
|
||||
text_input("Title...", &self.title).on_input(Message::ChangeTitle);
|
||||
|
||||
let image_selector = button::icon(
|
||||
icon::from_name("folder-images-symbolic").scale(2),
|
||||
)
|
||||
.label("Image")
|
||||
.tooltip("Select a image")
|
||||
.on_press(Message::PickImage)
|
||||
.padding(10);
|
||||
let image_selector =
|
||||
button::icon(icon::from_name("folder-images-symbolic").scale(2))
|
||||
.label("Image")
|
||||
.tooltip("Select a image")
|
||||
.on_press(Message::PickImage)
|
||||
.padding(10);
|
||||
|
||||
row![
|
||||
text::body("Title:"),
|
||||
|
|
@ -163,9 +150,7 @@ async fn pick_image() -> Result<PathBuf, ImageError> {
|
|||
error!(?e);
|
||||
ImageError::DialogClosed
|
||||
})
|
||||
.map(|file| {
|
||||
file.url().to_file_path().expect("Should be a file here")
|
||||
})
|
||||
.map(|file| file.url().to_file_path().expect("Should be a file here"))
|
||||
// rfd::AsyncFileDialog::new()
|
||||
// .set_title("Choose a background...")
|
||||
// .add_filter(
|
||||
|
|
|
|||
|
|
@ -10,11 +10,8 @@ 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(),
|
||||
);
|
||||
let (width, height, pixels) =
|
||||
(image.width(), image.height(), image.to_rgba8().to_vec());
|
||||
Ok(Handle::from_rgba(width, height, pixels))
|
||||
})
|
||||
.await
|
||||
|
|
@ -38,8 +35,7 @@ impl ImageLoader {
|
|||
.cloned()
|
||||
} else {
|
||||
self.decoding_images.insert(path.clone());
|
||||
let image =
|
||||
image::open(path).map_err(Error::ImageError)?;
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ use cosmic::iced::{Background, Border, Color, Length};
|
|||
use cosmic::widget::menu::{self, Action as MenuAction};
|
||||
use cosmic::widget::space::{self, horizontal};
|
||||
use cosmic::widget::{
|
||||
Container, DndSource, Space, button, container, context_menu,
|
||||
divider, dnd_destination, icon, mouse_area, row, scrollable,
|
||||
text, text_input,
|
||||
Container, DndSource, Space, button, container, context_menu, divider,
|
||||
dnd_destination, icon, mouse_area, row, scrollable, text, text_input,
|
||||
};
|
||||
use cosmic::{Element, Task, theme};
|
||||
use itertools::Itertools;
|
||||
|
|
@ -120,16 +119,10 @@ impl<'a> Library {
|
|||
error!(?e);
|
||||
}
|
||||
Self {
|
||||
song_library: Model::new_song_model(Arc::clone(&db))
|
||||
.await,
|
||||
image_library: Model::new_image_model(Arc::clone(&db))
|
||||
.await,
|
||||
video_library: Model::new_video_model(Arc::clone(&db))
|
||||
.await,
|
||||
presentation_library: Model::new_presentation_model(
|
||||
Arc::clone(&db),
|
||||
)
|
||||
.await,
|
||||
song_library: Model::new_song_model(Arc::clone(&db)).await,
|
||||
image_library: Model::new_image_model(Arc::clone(&db)).await,
|
||||
video_library: Model::new_video_model(Arc::clone(&db)).await,
|
||||
presentation_library: Model::new_presentation_model(Arc::clone(&db)).await,
|
||||
library_open: None,
|
||||
library_hovered: None,
|
||||
selected_items: None,
|
||||
|
|
@ -186,11 +179,10 @@ impl<'a> Library {
|
|||
return Action::CreateSong;
|
||||
}
|
||||
Message::AddSongFromEditor(song) => {
|
||||
let after_task =
|
||||
Task::done(Message::OpenItem(Some((
|
||||
LibraryKind::Song,
|
||||
self.song_library.items.len() as i32,
|
||||
))));
|
||||
let after_task = Task::done(Message::OpenItem(Some((
|
||||
LibraryKind::Song,
|
||||
self.song_library.items.len() as i32,
|
||||
))));
|
||||
debug!(?song);
|
||||
let task = Task::perform(
|
||||
insert_song(
|
||||
|
|
@ -198,15 +190,12 @@ impl<'a> Library {
|
|||
self.song_library.items.clone(),
|
||||
Arc::clone(&self.db),
|
||||
),
|
||||
|res| {
|
||||
res.map_or(Message::None, Message::ReaddSongs)
|
||||
},
|
||||
|res| res.map_or(Message::None, Message::ReaddSongs),
|
||||
);
|
||||
return Action::Task(task.chain(after_task));
|
||||
}
|
||||
Message::AddItem => {
|
||||
let kind =
|
||||
self.library_open.unwrap_or(LibraryKind::Song);
|
||||
let kind = self.library_open.unwrap_or(LibraryKind::Song);
|
||||
match kind {
|
||||
LibraryKind::Song => {
|
||||
return self.update(Message::AddSong);
|
||||
|
|
@ -236,11 +225,10 @@ impl<'a> Library {
|
|||
let _index = self.video_library.items.len();
|
||||
// Check if empty
|
||||
let mut tasks = Vec::new();
|
||||
let after_task =
|
||||
Task::done(Message::OpenItem(Some((
|
||||
LibraryKind::Video,
|
||||
self.video_library.items.len() as i32,
|
||||
))));
|
||||
let after_task = Task::done(Message::OpenItem(Some((
|
||||
LibraryKind::Video,
|
||||
self.video_library.items.len() as i32,
|
||||
))));
|
||||
if let Some(new_videos) = videos {
|
||||
let task = Task::perform(
|
||||
videos::add_video(
|
||||
|
|
@ -256,16 +244,13 @@ impl<'a> Library {
|
|||
);
|
||||
tasks.push(task);
|
||||
}
|
||||
return Action::Task(
|
||||
Task::batch(tasks).chain(after_task),
|
||||
);
|
||||
return Action::Task(Task::batch(tasks).chain(after_task));
|
||||
}
|
||||
Message::AddPresentationSplit(presentation) => {
|
||||
debug!(?presentation, "adding to db");
|
||||
if let Some(presentation) = presentation {
|
||||
if let Err(e) = self
|
||||
.presentation_library
|
||||
.add_item(presentation.clone())
|
||||
if let Err(e) =
|
||||
self.presentation_library.add_item(presentation.clone())
|
||||
{
|
||||
error!(?e);
|
||||
}
|
||||
|
|
@ -276,12 +261,9 @@ impl<'a> Library {
|
|||
Arc::clone(&self.db),
|
||||
),
|
||||
move |res| {
|
||||
res.map_or(
|
||||
Message::None,
|
||||
|presentations| {
|
||||
Message::ReaddPres(presentations)
|
||||
},
|
||||
)
|
||||
res.map_or(Message::None, |presentations| {
|
||||
Message::ReaddPres(presentations)
|
||||
})
|
||||
},
|
||||
));
|
||||
}
|
||||
|
|
@ -291,11 +273,10 @@ impl<'a> Library {
|
|||
let _index = self.presentation_library.items.len();
|
||||
// Check if empty
|
||||
let mut tasks = Vec::new();
|
||||
let after_task =
|
||||
Task::done(Message::OpenItem(Some((
|
||||
LibraryKind::Presentation,
|
||||
self.presentation_library.items.len() as i32,
|
||||
))));
|
||||
let after_task = Task::done(Message::OpenItem(Some((
|
||||
LibraryKind::Presentation,
|
||||
self.presentation_library.items.len() as i32,
|
||||
))));
|
||||
if let Some(new_presentations) = presentations {
|
||||
let task = Task::perform(
|
||||
presentations::add_presentation(
|
||||
|
|
@ -304,30 +285,24 @@ impl<'a> Library {
|
|||
Arc::clone(&self.db),
|
||||
),
|
||||
move |res| {
|
||||
res.map_or(
|
||||
Message::None,
|
||||
|presentations| {
|
||||
Message::ReaddPres(presentations)
|
||||
},
|
||||
)
|
||||
res.map_or(Message::None, |presentations| {
|
||||
Message::ReaddPres(presentations)
|
||||
})
|
||||
},
|
||||
);
|
||||
tasks.push(task);
|
||||
}
|
||||
return Action::Task(
|
||||
Task::batch(tasks).chain(after_task),
|
||||
);
|
||||
return Action::Task(Task::batch(tasks).chain(after_task));
|
||||
}
|
||||
Message::AddImages(images) => {
|
||||
debug!(?images, "adding to db");
|
||||
let _index = self.image_library.items.len();
|
||||
// Check if empty
|
||||
let mut tasks = Vec::new();
|
||||
let after_task =
|
||||
Task::done(Message::OpenItem(Some((
|
||||
LibraryKind::Image,
|
||||
self.image_library.items.len() as i32,
|
||||
))));
|
||||
let after_task = Task::done(Message::OpenItem(Some((
|
||||
LibraryKind::Image,
|
||||
self.image_library.items.len() as i32,
|
||||
))));
|
||||
if let Some(new_images) = images {
|
||||
let task = Task::perform(
|
||||
images::add_image(
|
||||
|
|
@ -343,9 +318,7 @@ impl<'a> Library {
|
|||
);
|
||||
tasks.push(task);
|
||||
}
|
||||
return Action::Task(
|
||||
Task::batch(tasks).chain(after_task),
|
||||
);
|
||||
return Action::Task(Task::batch(tasks).chain(after_task));
|
||||
}
|
||||
Message::OpenItem(item) => {
|
||||
debug!(?item);
|
||||
|
|
@ -359,8 +332,7 @@ impl<'a> Library {
|
|||
let Some(index) = self.context_menu else {
|
||||
return Action::None;
|
||||
};
|
||||
return self
|
||||
.update(Message::OpenItem(Some((kind, index))));
|
||||
return self.update(Message::OpenItem(Some((kind, index))));
|
||||
}
|
||||
Message::HoverLibrary(library_kind) => {
|
||||
self.library_hovered = library_kind;
|
||||
|
|
@ -391,12 +363,7 @@ impl<'a> Library {
|
|||
let Some(first_item) = self
|
||||
.selected_items
|
||||
.as_ref()
|
||||
.and_then(|items| {
|
||||
items
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(_, index)| index)
|
||||
})
|
||||
.and_then(|items| items.iter().next().map(|(_, index)| index))
|
||||
else {
|
||||
let Some(item) = item else {
|
||||
return Action::None;
|
||||
|
|
@ -409,20 +376,16 @@ impl<'a> Library {
|
|||
};
|
||||
if first_item < &index {
|
||||
for id in *first_item..=index {
|
||||
self.selected_items = self
|
||||
.selected_items
|
||||
.clone()
|
||||
.map(|mut items| {
|
||||
self.selected_items =
|
||||
self.selected_items.clone().map(|mut items| {
|
||||
items.push((kind, id));
|
||||
items
|
||||
});
|
||||
}
|
||||
} else if first_item > &index {
|
||||
for id in index..*first_item {
|
||||
self.selected_items = self
|
||||
.selected_items
|
||||
.clone()
|
||||
.map(|mut items| {
|
||||
self.selected_items =
|
||||
self.selected_items.clone().map(|mut items| {
|
||||
items.push((kind, id));
|
||||
items
|
||||
});
|
||||
|
|
@ -433,8 +396,7 @@ impl<'a> Library {
|
|||
let Some(item) = item else {
|
||||
return Action::None;
|
||||
};
|
||||
let Some(items) = self.selected_items.as_mut()
|
||||
else {
|
||||
let Some(items) = self.selected_items.as_mut() else {
|
||||
self.selected_items = Some(vec![item]);
|
||||
return Action::None;
|
||||
};
|
||||
|
|
@ -575,13 +537,11 @@ impl<'a> Library {
|
|||
|
||||
#[must_use]
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let cosmic::cosmic_theme::Spacing { space_s, .. } =
|
||||
cosmic::theme::spacing();
|
||||
let cosmic::cosmic_theme::Spacing { space_s, .. } = cosmic::theme::spacing();
|
||||
let song_library = self.library_item(&self.song_library);
|
||||
let image_library = self.library_item(&self.image_library);
|
||||
let video_library = self.library_item(&self.video_library);
|
||||
let presentation_library =
|
||||
self.library_item(&self.presentation_library);
|
||||
let presentation_library = self.library_item(&self.presentation_library);
|
||||
|
||||
let library_column = column![
|
||||
text::heading("Library").center().width(Length::Fill),
|
||||
|
|
@ -632,10 +592,7 @@ impl<'a> Library {
|
|||
let mut items = Vec::new();
|
||||
for line in text.lines() {
|
||||
let Ok(url) = url::Url::parse(line) else {
|
||||
error!(
|
||||
?line,
|
||||
"problem parsing this file url"
|
||||
);
|
||||
error!(?line, "problem parsing this file url");
|
||||
continue;
|
||||
};
|
||||
let Ok(path) = url.to_file_path() else {
|
||||
|
|
@ -657,47 +614,32 @@ impl<'a> Library {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn library_item<T>(
|
||||
&'a self,
|
||||
model: &'a Model<T>,
|
||||
) -> Element<'a, Message>
|
||||
pub fn library_item<T>(&'a self, model: &'a Model<T>) -> Element<'a, Message>
|
||||
where
|
||||
T: Content,
|
||||
{
|
||||
let mut row = row::with_capacity(5).spacing(5);
|
||||
match &model.kind {
|
||||
LibraryKind::Song => {
|
||||
row = row
|
||||
.push(icon::from_name("folder-music-symbolic"));
|
||||
row = row
|
||||
.push(textm!("Songs").align_y(Vertical::Center));
|
||||
row = row.push(icon::from_name("folder-music-symbolic"));
|
||||
row = row.push(textm!("Songs").align_y(Vertical::Center));
|
||||
}
|
||||
LibraryKind::Video => {
|
||||
row = row
|
||||
.push(icon::from_name("folder-videos-symbolic"));
|
||||
row = row
|
||||
.push(textm!("Videos").align_y(Vertical::Center));
|
||||
row = row.push(icon::from_name("folder-videos-symbolic"));
|
||||
row = row.push(textm!("Videos").align_y(Vertical::Center));
|
||||
}
|
||||
LibraryKind::Image => {
|
||||
row = row.push(icon::from_name(
|
||||
"folder-pictures-symbolic",
|
||||
));
|
||||
row = row
|
||||
.push(textm!("Images").align_y(Vertical::Center));
|
||||
row = row.push(icon::from_name("folder-pictures-symbolic"));
|
||||
row = row.push(textm!("Images").align_y(Vertical::Center));
|
||||
}
|
||||
LibraryKind::Presentation => {
|
||||
row = row.push(icon::from_name(
|
||||
"x-office-presentation-symbolic",
|
||||
));
|
||||
row = row.push(
|
||||
textm!("Presentations").align_y(Vertical::Center),
|
||||
);
|
||||
row = row.push(icon::from_name("x-office-presentation-symbolic"));
|
||||
row = row.push(textm!("Presentations").align_y(Vertical::Center));
|
||||
}
|
||||
}
|
||||
let item_count = model.items.len();
|
||||
row = row.push(space::horizontal());
|
||||
row = row
|
||||
.push(textm!("{}", item_count).align_y(Vertical::Center));
|
||||
row = row.push(textm!("{}", item_count).align_y(Vertical::Center));
|
||||
row = row.push(
|
||||
icon::from_name({
|
||||
if self.library_open == Some(model.kind) {
|
||||
|
|
@ -708,41 +650,26 @@ impl<'a> Library {
|
|||
})
|
||||
.size(20),
|
||||
);
|
||||
let row_container =
|
||||
Container::new(row.align_y(Vertical::Center))
|
||||
.padding(5)
|
||||
.style(|t| {
|
||||
container::Style::default()
|
||||
.background({
|
||||
self.library_hovered.map_or_else(
|
||||
|| {
|
||||
Background::Color(
|
||||
t.cosmic().button.base.into(),
|
||||
)
|
||||
},
|
||||
|library| {
|
||||
Background::Color(
|
||||
if library == model.kind {
|
||||
t.cosmic()
|
||||
.button
|
||||
.hover
|
||||
.into()
|
||||
} else {
|
||||
t.cosmic()
|
||||
.button
|
||||
.base
|
||||
.into()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.border(Border::default().rounded(
|
||||
t.cosmic().corner_radii.radius_s,
|
||||
))
|
||||
})
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Shrink);
|
||||
let row_container = Container::new(row.align_y(Vertical::Center))
|
||||
.padding(5)
|
||||
.style(|t| {
|
||||
container::Style::default()
|
||||
.background({
|
||||
self.library_hovered.map_or_else(
|
||||
|| Background::Color(t.cosmic().button.base.into()),
|
||||
|library| {
|
||||
Background::Color(if library == model.kind {
|
||||
t.cosmic().button.hover.into()
|
||||
} else {
|
||||
t.cosmic().button.base.into()
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
.border(Border::default().rounded(t.cosmic().corner_radii.radius_s))
|
||||
})
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Shrink);
|
||||
let library_button = mouse_area(row_container)
|
||||
.on_press({
|
||||
if self.library_open == Some(model.kind) {
|
||||
|
|
@ -756,79 +683,68 @@ impl<'a> Library {
|
|||
let lib_container = if self.library_open == Some(model.kind) {
|
||||
let items = scrollable(
|
||||
column({
|
||||
model.items.iter().enumerate().map(
|
||||
|(index, item)| {
|
||||
let i32_index = i32::try_from(index).expect("shouldn't be negative");
|
||||
let kind = model.kind;
|
||||
let visual_item = self
|
||||
.single_item(index, item, model);
|
||||
model.items.iter().enumerate().map(|(index, item)| {
|
||||
let i32_index =
|
||||
i32::try_from(index).expect("shouldn't be negative");
|
||||
let kind = model.kind;
|
||||
let visual_item = self.single_item(index, item, model);
|
||||
|
||||
DndSource::<Message, KindWrapper>::new({
|
||||
let mouse_area = mouse_area(visual_item);
|
||||
let mouse_area = mouse_area.on_enter(Message::HoverItem(
|
||||
Some((
|
||||
model.kind,
|
||||
i32_index ,
|
||||
)),
|
||||
))
|
||||
.on_double_click(
|
||||
Message::OpenItem(Some((
|
||||
model.kind,
|
||||
i32_index,
|
||||
))),
|
||||
)
|
||||
.on_right_press(Message::OpenContext(i32_index ))
|
||||
.on_exit(Message::HoverItem(None))
|
||||
.on_press(Message::SelectItem(
|
||||
Some((
|
||||
model.kind,
|
||||
i32_index,
|
||||
)),
|
||||
));
|
||||
DndSource::<Message, KindWrapper>::new({
|
||||
let mouse_area = mouse_area(visual_item);
|
||||
let mouse_area = mouse_area
|
||||
.on_enter(Message::HoverItem(Some((
|
||||
model.kind, i32_index,
|
||||
))))
|
||||
.on_double_click(Message::OpenItem(Some((
|
||||
model.kind, i32_index,
|
||||
))))
|
||||
.on_right_press(Message::OpenContext(i32_index))
|
||||
.on_exit(Message::HoverItem(None))
|
||||
.on_press(Message::SelectItem(Some((
|
||||
model.kind, i32_index,
|
||||
))));
|
||||
|
||||
Element::from(mouse_area)
|
||||
})
|
||||
.action(DndAction::Copy)
|
||||
.drag_icon({
|
||||
let model = model.kind;
|
||||
move |i| {
|
||||
let state = State::None;
|
||||
let icon = match model {
|
||||
LibraryKind::Song => icon::from_name(
|
||||
"folder-music-symbolic",
|
||||
).symbolic(true)
|
||||
,
|
||||
LibraryKind::Video => icon::from_name("folder-videos-symbolic"),
|
||||
LibraryKind::Image => icon::from_name("folder-pictures-symbolic"),
|
||||
LibraryKind::Presentation => icon::from_name("x-office-presentation-symbolic"),
|
||||
};
|
||||
(
|
||||
icon.into(),
|
||||
state,
|
||||
i,
|
||||
)
|
||||
}})
|
||||
.drag_content(move || {
|
||||
KindWrapper((kind, i32_index))
|
||||
})
|
||||
.into()
|
||||
},
|
||||
)
|
||||
Element::from(mouse_area)
|
||||
})
|
||||
.action(DndAction::Copy)
|
||||
.drag_icon({
|
||||
let model = model.kind;
|
||||
move |i| {
|
||||
let state = State::None;
|
||||
let icon = match model {
|
||||
LibraryKind::Song => {
|
||||
icon::from_name("folder-music-symbolic")
|
||||
.symbolic(true)
|
||||
}
|
||||
LibraryKind::Video => {
|
||||
icon::from_name("folder-videos-symbolic")
|
||||
}
|
||||
LibraryKind::Image => {
|
||||
icon::from_name("folder-pictures-symbolic")
|
||||
}
|
||||
LibraryKind::Presentation => {
|
||||
icon::from_name("x-office-presentation-symbolic")
|
||||
}
|
||||
};
|
||||
(icon.into(), state, i)
|
||||
}
|
||||
})
|
||||
.drag_content(move || KindWrapper((kind, i32_index)))
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.spacing(2)
|
||||
.width(Length::Fill),
|
||||
.spacing(2)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.spacing(5)
|
||||
.height(Length::Fill);
|
||||
|
||||
let library_toolbar = rowm!(
|
||||
text_input("Search...", ""),
|
||||
button::icon(icon::from_name("add"))
|
||||
.on_press(Message::AddItem)
|
||||
button::icon(icon::from_name("add")).on_press(Message::AddItem)
|
||||
);
|
||||
let context_menu = self.context_menu(items.into());
|
||||
let library_column =
|
||||
column![library_toolbar, context_menu].spacing(3);
|
||||
let library_column = column![library_toolbar, context_menu].spacing(3);
|
||||
Container::new(library_column).padding(5)
|
||||
} else {
|
||||
Container::new(Space::new())
|
||||
|
|
@ -851,9 +767,7 @@ impl<'a> Library {
|
|||
} = theme::spacing();
|
||||
let text = Container::new(
|
||||
text::heading(item.title())
|
||||
.ellipsize(Ellipsize::End(
|
||||
EllipsizeHeightLimit::Lines(1),
|
||||
))
|
||||
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
|
||||
.wrapping(textm::Wrapping::None),
|
||||
)
|
||||
.center_y(20)
|
||||
|
|
@ -863,35 +777,25 @@ impl<'a> Library {
|
|||
if let Some(items) = &self.selected_items
|
||||
&& items.contains(&(
|
||||
model.kind,
|
||||
i32::try_from(index)
|
||||
.expect("Should never be negative"),
|
||||
i32::try_from(index).expect("Should never be negative"),
|
||||
))
|
||||
{
|
||||
theme::active().cosmic().control_0().into()
|
||||
} else {
|
||||
theme::active()
|
||||
.cosmic()
|
||||
.accent_text_color()
|
||||
.into()
|
||||
theme::active().cosmic().accent_text_color().into()
|
||||
}
|
||||
} else if let Some(items) = &self.selected_items
|
||||
&& items.contains(&(
|
||||
model.kind,
|
||||
i32::try_from(index)
|
||||
.expect("Should never be negative"),
|
||||
i32::try_from(index).expect("Should never be negative"),
|
||||
))
|
||||
{
|
||||
theme::active().cosmic().control_0().into()
|
||||
} else {
|
||||
theme::active()
|
||||
.cosmic()
|
||||
.destructive_text_color()
|
||||
.into()
|
||||
theme::active().cosmic().destructive_text_color().into()
|
||||
};
|
||||
text::body(item.subtext())
|
||||
.ellipsize(Ellipsize::End(
|
||||
EllipsizeHeightLimit::Lines(1),
|
||||
))
|
||||
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
|
||||
.wrapping(textm::Wrapping::None)
|
||||
.class(color)
|
||||
})
|
||||
|
|
@ -921,12 +825,8 @@ impl<'a> Library {
|
|||
{
|
||||
if items.contains(&(model.kind, index)) {
|
||||
t.cosmic().accent.selected.into()
|
||||
} else if let Some((library, hovered)) =
|
||||
self.hovered_item
|
||||
{
|
||||
if model.kind == library
|
||||
&& hovered == index
|
||||
{
|
||||
} else if let Some((library, hovered)) = self.hovered_item {
|
||||
if model.kind == library && hovered == index {
|
||||
t.cosmic().button.hover.into()
|
||||
} else {
|
||||
t.cosmic().button.base.into()
|
||||
|
|
@ -934,8 +834,7 @@ impl<'a> Library {
|
|||
} else {
|
||||
t.cosmic().button.base.into()
|
||||
}
|
||||
} else if let Some((library, hovered)) =
|
||||
self.hovered_item
|
||||
} else if let Some((library, hovered)) = self.hovered_item
|
||||
&& let Ok(index) = i32::try_from(index)
|
||||
{
|
||||
if model.kind == library && hovered == index {
|
||||
|
|
@ -947,35 +846,23 @@ impl<'a> Library {
|
|||
t.cosmic().button.base.into()
|
||||
},
|
||||
))
|
||||
.border(
|
||||
Border::default()
|
||||
.rounded(t.cosmic().corner_radii.radius_m),
|
||||
)
|
||||
.border(Border::default().rounded(t.cosmic().corner_radii.radius_m))
|
||||
})
|
||||
.padding([space_xxs, space_s])
|
||||
.into()
|
||||
}
|
||||
|
||||
fn context_menu<'b>(
|
||||
&self,
|
||||
items: Element<'b, Message>,
|
||||
) -> Element<'b, Message> {
|
||||
fn context_menu<'b>(&self, items: Element<'b, Message>) -> Element<'b, Message> {
|
||||
if self.context_menu.is_some() {
|
||||
let menu_items = vec![
|
||||
menu::Item::Button("Open", None, MenuMessage::Open),
|
||||
menu::Item::Button(
|
||||
"Delete",
|
||||
None,
|
||||
MenuMessage::Delete,
|
||||
),
|
||||
menu::Item::Button("Delete", None, MenuMessage::Delete),
|
||||
];
|
||||
let context_menu = context_menu(
|
||||
items,
|
||||
self.context_menu.map_or_else(
|
||||
|| None,
|
||||
|_| {
|
||||
Some(menu::items(&self.menu_keys, menu_items))
|
||||
},
|
||||
|_| Some(menu::items(&self.menu_keys, menu_items)),
|
||||
),
|
||||
);
|
||||
Element::from(context_menu)
|
||||
|
|
@ -985,10 +872,7 @@ impl<'a> Library {
|
|||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn search_items(
|
||||
&self,
|
||||
query: String,
|
||||
) -> Vec<ServiceItemKind> {
|
||||
pub async fn search_items(&self, query: String) -> Vec<ServiceItemKind> {
|
||||
let query = query.to_lowercase();
|
||||
let items = self
|
||||
.song_library
|
||||
|
|
@ -1006,9 +890,7 @@ impl<'a> Library {
|
|||
.image_library
|
||||
.items
|
||||
.iter()
|
||||
.filter(|image| {
|
||||
image.title.to_lowercase().contains(&query)
|
||||
})
|
||||
.filter(|image| image.title.to_lowercase().contains(&query))
|
||||
.map(|image| ServiceItemKind::Image(image.clone()));
|
||||
let presentations = self
|
||||
.presentation_library
|
||||
|
|
@ -1022,10 +904,7 @@ impl<'a> Library {
|
|||
let mut items: Vec<(usize, ServiceItemKind)> = items
|
||||
.map(|item| {
|
||||
(
|
||||
levenshtein::distance(
|
||||
query.bytes(),
|
||||
item.title().bytes(),
|
||||
),
|
||||
levenshtein::distance(query.bytes(), item.title().bytes()),
|
||||
item,
|
||||
)
|
||||
})
|
||||
|
|
@ -1046,17 +925,11 @@ impl<'a> Library {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_presentation(
|
||||
&self,
|
||||
index: i32,
|
||||
) -> Option<&Presentation> {
|
||||
pub fn get_presentation(&self, index: i32) -> Option<&Presentation> {
|
||||
self.presentation_library.get_item(index)
|
||||
}
|
||||
|
||||
pub const fn set_modifiers(
|
||||
&mut self,
|
||||
modifiers: Option<Modifiers>,
|
||||
) {
|
||||
pub const fn set_modifiers(&mut self, modifiers: Option<Modifiers>) {
|
||||
self.modifiers_pressed = modifiers;
|
||||
}
|
||||
|
||||
|
|
@ -1080,8 +953,7 @@ impl<'a> Library {
|
|||
debug!(?ids);
|
||||
let task = match self.library_open {
|
||||
Some(LibraryKind::Song) => {
|
||||
let songs: Vec<Song> =
|
||||
self.song_library.items.drain(..).collect();
|
||||
let songs: Vec<Song> = self.song_library.items.drain(..).collect();
|
||||
|
||||
let ids = songs
|
||||
.iter()
|
||||
|
|
@ -1096,11 +968,7 @@ impl<'a> Library {
|
|||
.collect();
|
||||
|
||||
Task::perform(
|
||||
songs::remove_songs(
|
||||
Arc::clone(&self.db),
|
||||
songs,
|
||||
ids,
|
||||
),
|
||||
songs::remove_songs(Arc::clone(&self.db), songs, ids),
|
||||
|r| match r {
|
||||
Ok(songs) => Message::ReaddSongs(songs),
|
||||
Err(e) => {
|
||||
|
|
@ -1111,8 +979,7 @@ impl<'a> Library {
|
|||
)
|
||||
}
|
||||
Some(LibraryKind::Video) => {
|
||||
let videos: Vec<Video> =
|
||||
self.video_library.items.drain(..).collect();
|
||||
let videos: Vec<Video> = self.video_library.items.drain(..).collect();
|
||||
|
||||
let ids = videos
|
||||
.iter()
|
||||
|
|
@ -1127,11 +994,7 @@ impl<'a> Library {
|
|||
.collect();
|
||||
|
||||
Task::perform(
|
||||
videos::remove_videos(
|
||||
Arc::clone(&self.db),
|
||||
videos.clone(),
|
||||
ids,
|
||||
),
|
||||
videos::remove_videos(Arc::clone(&self.db), videos.clone(), ids),
|
||||
|r| match r {
|
||||
Ok(videos) => Message::ReaddVideos(videos),
|
||||
Err(e) => {
|
||||
|
|
@ -1142,8 +1005,7 @@ impl<'a> Library {
|
|||
)
|
||||
}
|
||||
Some(LibraryKind::Image) => {
|
||||
let images: Vec<Image> =
|
||||
self.image_library.items.drain(..).collect();
|
||||
let images: Vec<Image> = self.image_library.items.drain(..).collect();
|
||||
|
||||
let ids = images
|
||||
.iter()
|
||||
|
|
@ -1158,11 +1020,7 @@ impl<'a> Library {
|
|||
.collect();
|
||||
|
||||
Task::perform(
|
||||
images::remove_images(
|
||||
Arc::clone(&self.db),
|
||||
images,
|
||||
ids,
|
||||
),
|
||||
images::remove_images(Arc::clone(&self.db), images, ids),
|
||||
|r| match r {
|
||||
Ok(images) => Message::ReaddImages(images),
|
||||
Err(e) => {
|
||||
|
|
@ -1173,11 +1031,8 @@ impl<'a> Library {
|
|||
)
|
||||
}
|
||||
Some(LibraryKind::Presentation) => {
|
||||
let presentations: Vec<Presentation> = self
|
||||
.presentation_library
|
||||
.items
|
||||
.drain(..)
|
||||
.collect();
|
||||
let presentations: Vec<Presentation> =
|
||||
self.presentation_library.items.drain(..).collect();
|
||||
|
||||
let ids = presentations
|
||||
.iter()
|
||||
|
|
@ -1198,9 +1053,7 @@ impl<'a> Library {
|
|||
ids,
|
||||
),
|
||||
|r| match r {
|
||||
Ok(presentations) => {
|
||||
Message::ReaddPres(presentations)
|
||||
}
|
||||
Ok(presentations) => Message::ReaddPres(presentations),
|
||||
Err(e) => {
|
||||
error!(?e);
|
||||
Message::None
|
||||
|
|
@ -1247,9 +1100,7 @@ impl<'a> Library {
|
|||
.items
|
||||
.len()
|
||||
.try_into()
|
||||
.expect(
|
||||
"too many items in presentation_library",
|
||||
),
|
||||
.expect("too many items in presentation_library"),
|
||||
))))
|
||||
}
|
||||
_ => Task::none(),
|
||||
|
|
@ -1278,9 +1129,7 @@ impl<'a> Library {
|
|||
Arc::clone(&self.db),
|
||||
),
|
||||
move |res| {
|
||||
res.map_or(Message::None, |videos| {
|
||||
Message::ReaddVideos(videos)
|
||||
})
|
||||
res.map_or(Message::None, |videos| Message::ReaddVideos(videos))
|
||||
},
|
||||
));
|
||||
}
|
||||
|
|
@ -1308,9 +1157,7 @@ impl<'a> Library {
|
|||
Arc::clone(&self.db),
|
||||
),
|
||||
move |res| {
|
||||
res.map_or(Message::None, |images| {
|
||||
Message::ReaddImages(images)
|
||||
})
|
||||
res.map_or(Message::None, |images| Message::ReaddImages(images))
|
||||
},
|
||||
));
|
||||
}
|
||||
|
|
@ -1320,29 +1167,23 @@ impl<'a> Library {
|
|||
}
|
||||
|
||||
async fn add_images() -> Option<Vec<Image>> {
|
||||
let paths =
|
||||
Dialog::new().title("pick image").open_files().await.ok()?;
|
||||
let paths = Dialog::new().title("pick image").open_files().await.ok()?;
|
||||
Some(
|
||||
paths
|
||||
.urls()
|
||||
.iter()
|
||||
.map(|path| {
|
||||
Image::from(path.to_file_path().expect("oops"))
|
||||
})
|
||||
.map(|path| Image::from(path.to_file_path().expect("oops")))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn add_videos() -> Option<Vec<Video>> {
|
||||
let paths =
|
||||
Dialog::new().title("pick video").open_files().await.ok()?;
|
||||
let paths = Dialog::new().title("pick video").open_files().await.ok()?;
|
||||
Some(
|
||||
paths
|
||||
.urls()
|
||||
.iter()
|
||||
.map(|path| {
|
||||
Video::from(path.to_file_path().expect("oops"))
|
||||
})
|
||||
.map(|path| Video::from(path.to_file_path().expect("oops")))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
|
@ -1357,22 +1198,17 @@ async fn add_presentations() -> Option<Vec<Presentation>> {
|
|||
paths
|
||||
.urls()
|
||||
.iter()
|
||||
.map(|path| {
|
||||
Presentation::from(path.to_file_path().expect("oops"))
|
||||
})
|
||||
.map(|path| Presentation::from(path.to_file_path().expect("oops")))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn add_db() -> Result<SqlitePool> {
|
||||
let mut data = dirs::data_local_dir()
|
||||
.expect("Should always find a data dir");
|
||||
let mut data = dirs::data_local_dir().expect("Should always find a data dir");
|
||||
data.push("lumina");
|
||||
data.push("library-db.sqlite3");
|
||||
let mut db_url = String::from("sqlite://");
|
||||
db_url.push_str(
|
||||
data.to_str().expect("Should always be a file here"),
|
||||
);
|
||||
db_url.push_str(data.to_str().expect("Should always be a file here"));
|
||||
SqlitePool::connect(&db_url).await.into_diagnostic()
|
||||
}
|
||||
|
||||
|
|
@ -1385,20 +1221,16 @@ pub fn elide_text(text: impl AsRef<str>, width: f32) -> String {
|
|||
if text_length > width {
|
||||
format!(
|
||||
"{}...",
|
||||
if let Some((first, _second)) = text.split_at_checked(
|
||||
((width / CHAR_SIZE) - 3.0).floor() as usize
|
||||
) {
|
||||
first
|
||||
} else if let Some((first, _second)) = text
|
||||
.split_at_checked(
|
||||
((width / CHAR_SIZE) - 5.0).floor() as usize
|
||||
)
|
||||
if let Some((first, _second)) =
|
||||
text.split_at_checked(((width / CHAR_SIZE) - 3.0).floor() as usize)
|
||||
{
|
||||
first
|
||||
} else if let Some((first, _second)) = text
|
||||
.split_at_checked(
|
||||
((width / CHAR_SIZE) - 7.0).floor() as usize
|
||||
)
|
||||
} else if let Some((first, _second)) =
|
||||
text.split_at_checked(((width / CHAR_SIZE) - 5.0).floor() as usize)
|
||||
{
|
||||
first
|
||||
} else if let Some((first, _second)) =
|
||||
text.split_at_checked(((width / CHAR_SIZE) - 7.0).floor() as usize)
|
||||
{
|
||||
first
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ use cosmic::iced::{Background, ContentFit, Length};
|
|||
use cosmic::widget::image::Handle;
|
||||
use cosmic::widget::space::{self, horizontal};
|
||||
use cosmic::widget::{
|
||||
self, Space, button, container, context_menu, icon, menu,
|
||||
mouse_area, 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};
|
||||
|
|
@ -126,8 +126,7 @@ impl PresentationEditor {
|
|||
if let Some(presentation) = &self.presentation {
|
||||
let mut presentation = presentation.clone();
|
||||
presentation.title = title;
|
||||
return self
|
||||
.update(Message::Update(presentation));
|
||||
return self.update(Message::Update(presentation));
|
||||
}
|
||||
}
|
||||
Message::Edit(edit) => {
|
||||
|
|
@ -140,27 +139,16 @@ impl PresentationEditor {
|
|||
return Action::UpdatePresentation(presentation);
|
||||
}
|
||||
Message::PickPresentation => {
|
||||
let presentation_id = self
|
||||
.presentation
|
||||
.as_ref()
|
||||
.map(|v| v.id)
|
||||
.unwrap_or_default();
|
||||
let task = Task::perform(
|
||||
pick_presentation(),
|
||||
move |presentation_result| {
|
||||
presentation_result.map_or(
|
||||
Message::None,
|
||||
|presentation| {
|
||||
let mut presentation =
|
||||
Presentation::from(presentation);
|
||||
presentation.id = presentation_id;
|
||||
Message::ChangePresentationFile(
|
||||
presentation,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
let presentation_id =
|
||||
self.presentation.as_ref().map(|v| v.id).unwrap_or_default();
|
||||
let task =
|
||||
Task::perform(pick_presentation(), move |presentation_result| {
|
||||
presentation_result.map_or(Message::None, |presentation| {
|
||||
let mut presentation = Presentation::from(presentation);
|
||||
presentation.id = presentation_id;
|
||||
Message::ChangePresentationFile(presentation)
|
||||
})
|
||||
});
|
||||
return Action::Task(task);
|
||||
}
|
||||
Message::ChangePresentationFile(presentation) => {
|
||||
|
|
@ -185,9 +173,7 @@ impl PresentationEditor {
|
|||
);
|
||||
}
|
||||
|
||||
task = task.chain(Task::done(Message::Update(
|
||||
presentation.clone(),
|
||||
)));
|
||||
task = task.chain(Task::done(Message::Update(presentation.clone())));
|
||||
return Action::Task(task);
|
||||
}
|
||||
}
|
||||
|
|
@ -196,13 +182,10 @@ impl PresentationEditor {
|
|||
}
|
||||
Message::None => (),
|
||||
Message::NextPage => {
|
||||
let next_index =
|
||||
self.current_slide_index.unwrap_or_default() + 1;
|
||||
let next_index = self.current_slide_index.unwrap_or_default() + 1;
|
||||
|
||||
let last_index = if let Some(presentation) =
|
||||
self.presentation.as_ref()
|
||||
&& let PresKind::Pdf { ending_index, .. } =
|
||||
presentation.kind
|
||||
let last_index = if let Some(presentation) = self.presentation.as_ref()
|
||||
&& let PresKind::Pdf { ending_index, .. } = presentation.kind
|
||||
{
|
||||
ending_index
|
||||
} else {
|
||||
|
|
@ -212,42 +195,31 @@ impl PresentationEditor {
|
|||
if next_index > last_index {
|
||||
return Action::None;
|
||||
}
|
||||
self.current_slide =
|
||||
self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(next_index).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let Ok(pixmap) = page
|
||||
.to_pixmap(
|
||||
&matrix,
|
||||
&colorspace,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.into_diagnostic()
|
||||
else {
|
||||
error!(
|
||||
"Can't turn this page into pixmap"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
debug!(?pixmap);
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(next_index).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let Ok(pixmap) = page
|
||||
.to_pixmap(&matrix, &colorspace, true, true)
|
||||
.into_diagnostic()
|
||||
else {
|
||||
error!("Can't turn this page into pixmap");
|
||||
return None;
|
||||
};
|
||||
debug!(?pixmap);
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = Some(next_index);
|
||||
}
|
||||
Message::PrevPage => {
|
||||
let previous_index =
|
||||
self.current_slide_index.unwrap_or_default() - 1;
|
||||
let previous_index = self.current_slide_index.unwrap_or_default() - 1;
|
||||
|
||||
let first_index = if let Some(presentation) =
|
||||
self.presentation.as_ref()
|
||||
&& let PresKind::Pdf { starting_index, .. } =
|
||||
presentation.kind
|
||||
let first_index = if let Some(presentation) = self.presentation.as_ref()
|
||||
&& let PresKind::Pdf { starting_index, .. } = presentation.kind
|
||||
{
|
||||
starting_index
|
||||
} else {
|
||||
|
|
@ -257,65 +229,44 @@ impl PresentationEditor {
|
|||
if previous_index < first_index {
|
||||
return Action::None;
|
||||
}
|
||||
self.current_slide =
|
||||
self.document.as_ref().and_then(|doc| {
|
||||
let page =
|
||||
doc.load_page(previous_index).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
.to_pixmap(
|
||||
&matrix,
|
||||
&colorspace,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.ok()?;
|
||||
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(previous_index).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = Some(previous_index);
|
||||
}
|
||||
Message::ChangeSlide(index) => {
|
||||
let starting_index = if let Some(presentation) =
|
||||
self.presentation.as_ref()
|
||||
&& let PresKind::Pdf { starting_index, .. } =
|
||||
presentation.kind
|
||||
&& let PresKind::Pdf { starting_index, .. } = presentation.kind
|
||||
{
|
||||
starting_index
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
self.current_slide =
|
||||
self.document.as_ref().and_then(|doc| {
|
||||
let page = doc
|
||||
.load_page(
|
||||
i32::try_from(index).ok()?
|
||||
+ starting_index,
|
||||
)
|
||||
.ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
.to_pixmap(
|
||||
&matrix,
|
||||
&colorspace,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.ok()?;
|
||||
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||
let page = doc
|
||||
.load_page(i32::try_from(index).ok()? + starting_index)
|
||||
.ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = i32::try_from(index).ok();
|
||||
}
|
||||
Message::HoverSlide(slide) => {
|
||||
|
|
@ -328,18 +279,14 @@ impl PresentationEditor {
|
|||
if let Ok((first, second)) = self.split_before() {
|
||||
debug!(?first, ?second);
|
||||
self.update_entire_presentation(&first);
|
||||
return Action::SplitAddPresentation((
|
||||
first, second,
|
||||
));
|
||||
return Action::SplitAddPresentation((first, second));
|
||||
}
|
||||
}
|
||||
Message::SplitAfter => {
|
||||
if let Ok((first, second)) = self.split_after() {
|
||||
debug!(?first, ?second);
|
||||
self.update_entire_presentation(&first);
|
||||
return Action::SplitAddPresentation((
|
||||
first, second,
|
||||
));
|
||||
return Action::SplitAddPresentation((first, second));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -357,67 +304,56 @@ impl PresentationEditor {
|
|||
.into(),
|
||||
))
|
||||
.style(|_| {
|
||||
container::background(Background::Color(
|
||||
cosmic::iced::Color::WHITE,
|
||||
))
|
||||
container::background(Background::Color(cosmic::iced::Color::WHITE))
|
||||
})
|
||||
},
|
||||
);
|
||||
let pdf_pages: Vec<Element<Message>> =
|
||||
self.slides.as_ref().map_or_else(
|
||||
|| vec![horizontal().into()],
|
||||
|pages| {
|
||||
pages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, page)| {
|
||||
let image = loaded_image(page.clone(), widget::image(page)
|
||||
.height(
|
||||
theme::spacing().space_xxxl * 3,
|
||||
)
|
||||
.content_fit(ContentFit::ScaleDown).into());
|
||||
let pdf_pages: Vec<Element<Message>> = self.slides.as_ref().map_or_else(
|
||||
|| vec![horizontal().into()],
|
||||
|pages| {
|
||||
pages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, page)| {
|
||||
let image = loaded_image(
|
||||
page.clone(),
|
||||
widget::image(page)
|
||||
.height(theme::spacing().space_xxxl * 3)
|
||||
.content_fit(ContentFit::ScaleDown)
|
||||
.into(),
|
||||
);
|
||||
|
||||
let slide = container(image).style(|_| {
|
||||
container::background(Background::Color(
|
||||
cosmic::iced::Color::WHITE,
|
||||
))
|
||||
});
|
||||
let clickable_slide = container(
|
||||
mouse_area(slide)
|
||||
.on_enter(Message::HoverSlide(
|
||||
i32::try_from(index).ok(),
|
||||
))
|
||||
.on_exit(Message::HoverSlide(
|
||||
None,
|
||||
))
|
||||
.on_right_press(
|
||||
Message::ContextMenu(index),
|
||||
)
|
||||
.on_press(Message::ChangeSlide(
|
||||
index,
|
||||
)),
|
||||
)
|
||||
.padding(theme::spacing().space_m)
|
||||
.clip(true)
|
||||
.class(self.hovered_slide.map_or(
|
||||
theme::Container::Card,
|
||||
|hovered_index| {
|
||||
if i32::try_from(index).is_ok_and(
|
||||
|index| {
|
||||
index == hovered_index
|
||||
},
|
||||
) {
|
||||
theme::Container::Primary
|
||||
} else {
|
||||
theme::Container::Card
|
||||
}
|
||||
},
|
||||
));
|
||||
clickable_slide.into()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
let slide = container(image).style(|_| {
|
||||
container::background(Background::Color(
|
||||
cosmic::iced::Color::WHITE,
|
||||
))
|
||||
});
|
||||
let clickable_slide = container(
|
||||
mouse_area(slide)
|
||||
.on_enter(Message::HoverSlide(i32::try_from(index).ok()))
|
||||
.on_exit(Message::HoverSlide(None))
|
||||
.on_right_press(Message::ContextMenu(index))
|
||||
.on_press(Message::ChangeSlide(index)),
|
||||
)
|
||||
.padding(theme::spacing().space_m)
|
||||
.clip(true)
|
||||
.class(self.hovered_slide.map_or(
|
||||
theme::Container::Card,
|
||||
|hovered_index| {
|
||||
if i32::try_from(index)
|
||||
.is_ok_and(|index| index == hovered_index)
|
||||
{
|
||||
theme::Container::Primary
|
||||
} else {
|
||||
theme::Container::Card
|
||||
}
|
||||
},
|
||||
));
|
||||
clickable_slide.into()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
);
|
||||
let pages_column = container(
|
||||
self.context_menu(
|
||||
scrollable(
|
||||
|
|
@ -435,14 +371,12 @@ impl PresentationEditor {
|
|||
]
|
||||
.spacing(theme::spacing().space_xxl);
|
||||
let control_buttons = row![
|
||||
button::standard("Previous Page")
|
||||
.on_press(Message::PrevPage),
|
||||
button::standard("Previous Page").on_press(Message::PrevPage),
|
||||
space::horizontal(),
|
||||
button::standard("Next Page").on_press(Message::NextPage),
|
||||
];
|
||||
let column =
|
||||
column![self.toolbar(), main_row, control_buttons]
|
||||
.spacing(theme::active().cosmic().space_l());
|
||||
let column = column![self.toolbar(), main_row, control_buttons]
|
||||
.spacing(theme::active().cosmic().space_l());
|
||||
column.into()
|
||||
}
|
||||
|
||||
|
|
@ -455,13 +389,12 @@ impl PresentationEditor {
|
|||
)
|
||||
.on_input(Message::ChangeTitle);
|
||||
|
||||
let presentation_selector = button::icon(
|
||||
icon::from_name("folder-presentations-symbolic").scale(2),
|
||||
)
|
||||
.label("Change Presentation")
|
||||
.tooltip("Select a presentation")
|
||||
.on_press(Message::PickPresentation)
|
||||
.padding(10);
|
||||
let presentation_selector =
|
||||
button::icon(icon::from_name("folder-presentations-symbolic").scale(2))
|
||||
.label("Change Presentation")
|
||||
.tooltip("Select a presentation")
|
||||
.on_press(Message::PickPresentation)
|
||||
.padding(10);
|
||||
|
||||
row![
|
||||
text::body("Title:"),
|
||||
|
|
@ -478,17 +411,12 @@ impl PresentationEditor {
|
|||
self.editing
|
||||
}
|
||||
|
||||
fn context_menu<'b>(
|
||||
&self,
|
||||
items: Element<'b, Message>,
|
||||
) -> Element<'b, Message> {
|
||||
fn context_menu<'b>(&self, items: Element<'b, Message>) -> Element<'b, Message> {
|
||||
if self.context_menu_id.is_some() {
|
||||
let before_icon =
|
||||
icon::from_path("./res/split-above.svg".into())
|
||||
.symbolic(true);
|
||||
icon::from_path("./res/split-above.svg".into()).symbolic(true);
|
||||
let after_icon =
|
||||
icon::from_path("./res/split-below.svg".into())
|
||||
.symbolic(true);
|
||||
icon::from_path("./res/split-below.svg".into()).symbolic(true);
|
||||
let menu_items = vec![
|
||||
menu::Item::Button(
|
||||
"Spit Before",
|
||||
|
|
@ -505,9 +433,7 @@ impl PresentationEditor {
|
|||
items,
|
||||
self.context_menu_id.map_or_else(
|
||||
|| None,
|
||||
|_| {
|
||||
Some(menu::items(&HashMap::new(), menu_items))
|
||||
},
|
||||
|_| Some(menu::items(&HashMap::new(), menu_items)),
|
||||
),
|
||||
);
|
||||
Element::from(context_menu)
|
||||
|
|
@ -516,60 +442,44 @@ impl PresentationEditor {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_entire_presentation(
|
||||
&mut self,
|
||||
presentation: &Presentation,
|
||||
) {
|
||||
fn update_entire_presentation(&mut self, presentation: &Presentation) {
|
||||
self.presentation = Some(presentation.clone());
|
||||
self.title.clone_from(&presentation.title);
|
||||
self.document =
|
||||
Document::open(&presentation.path.as_path()).ok();
|
||||
self.page_count = self
|
||||
.document
|
||||
.as_ref()
|
||||
.and_then(|doc| doc.page_count().ok());
|
||||
self.document = Document::open(&presentation.path.as_path()).ok();
|
||||
self.page_count = self.document.as_ref().and_then(|doc| doc.page_count().ok());
|
||||
warn!("changing presentation");
|
||||
let pages = if let PresKind::Pdf {
|
||||
starting_index,
|
||||
ending_index,
|
||||
} = presentation.kind
|
||||
{
|
||||
self.current_slide =
|
||||
self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(starting_index).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
.to_pixmap(&matrix, &colorspace, true, true)
|
||||
.ok()?;
|
||||
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(starting_index).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = Some(starting_index);
|
||||
get_pages(
|
||||
starting_index..=ending_index,
|
||||
presentation.path.clone(),
|
||||
)
|
||||
get_pages(starting_index..=ending_index, presentation.path.clone())
|
||||
} else {
|
||||
self.current_slide =
|
||||
self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(0).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
.to_pixmap(&matrix, &colorspace, true, true)
|
||||
.ok()?;
|
||||
self.current_slide = self.document.as_ref().and_then(|doc| {
|
||||
let page = doc.load_page(0).ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
pixmap.height(),
|
||||
pixmap.samples().to_vec(),
|
||||
))
|
||||
});
|
||||
self.current_slide_index = Some(0);
|
||||
get_pages(.., presentation.path.clone())
|
||||
};
|
||||
|
|
@ -578,12 +488,8 @@ impl PresentationEditor {
|
|||
|
||||
fn split_before(&self) -> Result<(Presentation, Presentation)> {
|
||||
if let Some(index) = self.context_menu_id {
|
||||
let Some(current_presentation) =
|
||||
self.presentation.as_ref()
|
||||
else {
|
||||
return Err(miette!(
|
||||
"There is no current presentation"
|
||||
));
|
||||
let Some(current_presentation) = self.presentation.as_ref() else {
|
||||
return Err(miette!("There is no current presentation"));
|
||||
};
|
||||
let first_presentation = Presentation {
|
||||
id: current_presentation.id,
|
||||
|
|
@ -599,18 +505,13 @@ impl PresentationEditor {
|
|||
};
|
||||
let second_presentation = Presentation {
|
||||
id: 0,
|
||||
title: format!(
|
||||
"{} (2)",
|
||||
current_presentation.title.clone()
|
||||
),
|
||||
title: format!("{} (2)", current_presentation.title.clone()),
|
||||
path: current_presentation.path.clone(),
|
||||
kind: match current_presentation.kind {
|
||||
PresKind::Pdf { ending_index, .. } => {
|
||||
PresKind::Pdf {
|
||||
starting_index: index,
|
||||
ending_index,
|
||||
}
|
||||
}
|
||||
PresKind::Pdf { ending_index, .. } => PresKind::Pdf {
|
||||
starting_index: index,
|
||||
ending_index,
|
||||
},
|
||||
_ => current_presentation.kind.clone(),
|
||||
},
|
||||
};
|
||||
|
|
@ -625,12 +526,8 @@ impl PresentationEditor {
|
|||
|
||||
fn split_after(&self) -> Result<(Presentation, Presentation)> {
|
||||
if let Some(index) = self.context_menu_id {
|
||||
let Some(current_presentation) =
|
||||
self.presentation.as_ref()
|
||||
else {
|
||||
return Err(miette!(
|
||||
"There is no current presentation"
|
||||
));
|
||||
let Some(current_presentation) = self.presentation.as_ref() else {
|
||||
return Err(miette!("There is no current presentation"));
|
||||
};
|
||||
let first_presentation = Presentation {
|
||||
id: current_presentation.id,
|
||||
|
|
@ -646,18 +543,13 @@ impl PresentationEditor {
|
|||
};
|
||||
let second_presentation = Presentation {
|
||||
id: 0,
|
||||
title: format!(
|
||||
"{} (2)",
|
||||
current_presentation.title.clone()
|
||||
),
|
||||
title: format!("{} (2)", current_presentation.title.clone()),
|
||||
path: current_presentation.path.clone(),
|
||||
kind: match current_presentation.kind {
|
||||
PresKind::Pdf { ending_index, .. } => {
|
||||
PresKind::Pdf {
|
||||
starting_index: index + 1,
|
||||
ending_index,
|
||||
}
|
||||
}
|
||||
PresKind::Pdf { ending_index, .. } => PresKind::Pdf {
|
||||
starting_index: index + 1,
|
||||
ending_index,
|
||||
},
|
||||
_ => current_presentation.kind.clone(),
|
||||
},
|
||||
};
|
||||
|
|
@ -687,17 +579,16 @@ fn get_pages(
|
|||
pages
|
||||
.enumerate()
|
||||
.filter_map(|(index, page)| {
|
||||
if !range.contains(&i32::try_from(index).expect(
|
||||
"looking for a pdf index that is way too large",
|
||||
)) {
|
||||
if !range.contains(
|
||||
&i32::try_from(index)
|
||||
.expect("looking for a pdf index that is way too large"),
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let page = page.ok()?;
|
||||
let matrix = Matrix::IDENTITY;
|
||||
let colorspace = Colorspace::device_rgb();
|
||||
let pixmap = page
|
||||
.to_pixmap(&matrix, &colorspace, true, true)
|
||||
.ok()?;
|
||||
let pixmap = page.to_pixmap(&matrix, &colorspace, true, true).ok()?;
|
||||
|
||||
Some(Handle::from_rgba(
|
||||
pixmap.width(),
|
||||
|
|
@ -723,9 +614,7 @@ async fn pick_presentation() -> Result<PathBuf, PresentationError> {
|
|||
error!(?e);
|
||||
PresentationError::DialogClosed
|
||||
})
|
||||
.map(|file| {
|
||||
file.url().to_file_path().expect("Should be a file here")
|
||||
})
|
||||
.map(|file| file.url().to_file_path().expect("Should be a file here"))
|
||||
// rfd::AsyncFileDialog::new()
|
||||
// .set_title("Choose a background...")
|
||||
// .add_filter(
|
||||
|
|
|
|||
|
|
@ -9,20 +9,18 @@ use cosmic::cosmic_theme::Spacing;
|
|||
use cosmic::iced::alignment::Horizontal;
|
||||
use cosmic::iced::core::text::Alignment;
|
||||
use cosmic::iced::font::{Family, Stretch, Style, Weight};
|
||||
use cosmic::iced::widget::scrollable::{
|
||||
AbsoluteOffset, Direction, Scrollbar, scroll_to,
|
||||
};
|
||||
use cosmic::iced::widget::scrollable::{AbsoluteOffset, Direction, Scrollbar, scroll_to};
|
||||
use cosmic::iced::widget::stack;
|
||||
use cosmic::iced::{
|
||||
Animation, Background, Border, Color, ContentFit, Font, Length,
|
||||
Point, Shadow, Vector, animation,
|
||||
Animation, Background, Border, Color, ContentFit, Font, Length, Point, Shadow,
|
||||
Vector, animation,
|
||||
};
|
||||
use cosmic::prelude::*;
|
||||
use cosmic::widget::divider::{self, vertical};
|
||||
use cosmic::widget::{
|
||||
Container, Id, JustifyContent, Row, Space, column, container,
|
||||
flex_row, image as cosmic_image, menu, mouse_area, popover,
|
||||
responsive, scrollable, space, text,
|
||||
Container, Id, JustifyContent, Row, Space, column, container, flex_row,
|
||||
image as cosmic_image, menu, mouse_area, popover, responsive, scrollable, space,
|
||||
text,
|
||||
};
|
||||
use cosmic::{Task, theme};
|
||||
use derive_more::Debug;
|
||||
|
|
@ -37,7 +35,7 @@ use crate::BackgroundKind;
|
|||
use crate::core::service_items::ServiceItem;
|
||||
use crate::core::slide::Slide;
|
||||
use crate::core::slide_actions::{self, ObsAction};
|
||||
use crate::ui::gst_video;
|
||||
use crate::ui::gst_video::{self, VideoSettings};
|
||||
use crate::ui::image_loader::ImageLoader;
|
||||
use crate::ui::library::elide_text;
|
||||
use crate::ui::widgets::loaded_image::loaded_image;
|
||||
|
|
@ -65,8 +63,7 @@ pub(crate) struct Presenter {
|
|||
hovered_point: Option<Point>,
|
||||
scroll_id: Id,
|
||||
current_font: Font,
|
||||
slide_action_map:
|
||||
Option<HashMap<(usize, usize), Vec<slide_actions::Action>>>,
|
||||
slide_action_map: Option<HashMap<(usize, usize), Vec<slide_actions::Action>>>,
|
||||
obs_client: Option<Arc<Client>>,
|
||||
context_menu_id: Option<(usize, usize)>,
|
||||
context_point: Point,
|
||||
|
|
@ -129,7 +126,8 @@ impl Presenter {
|
|||
"There should be a video file here",
|
||||
);
|
||||
|
||||
let result = gst_video::create_video(&url, 15);
|
||||
let video_settings = VideoSettings { mute: true, framerate: 15 };
|
||||
let result = gst_video::create_video(&url, &video_settings);
|
||||
match result {
|
||||
Ok(mut v) => {
|
||||
v.set_paused(true);
|
||||
|
|
@ -157,7 +155,8 @@ impl Presenter {
|
|||
"There should be a video file here",
|
||||
);
|
||||
|
||||
let result = gst_video::create_video(&url, 60);
|
||||
let video_settings = VideoSettings { mute: false, framerate: 60 };
|
||||
let result = gst_video::create_video(&url, &video_settings);
|
||||
match result {
|
||||
Ok(mut v) => {
|
||||
v.set_paused(true);
|
||||
|
|
@ -173,11 +172,9 @@ impl Presenter {
|
|||
})
|
||||
})
|
||||
};
|
||||
let total_slides: usize =
|
||||
items.iter().fold(0, |a, item| a + item.slides.len());
|
||||
let total_slides: usize = items.iter().fold(0, |a, item| a + item.slides.len());
|
||||
|
||||
let slide =
|
||||
items.first().and_then(|item| item.slides.first());
|
||||
let slide = items.first().and_then(|item| item.slides.first());
|
||||
|
||||
let audio = items
|
||||
.first()
|
||||
|
|
@ -204,12 +201,9 @@ impl Presenter {
|
|||
hovered_slide: None,
|
||||
hovered_point: None,
|
||||
sink: {
|
||||
let stream_handle =
|
||||
rodio::DeviceSinkBuilder::open_default_sink()
|
||||
.expect("Can't open default rodio stream");
|
||||
let player = Arc::new(rodio::Player::connect_new(
|
||||
stream_handle.mixer(),
|
||||
));
|
||||
let stream_handle = rodio::DeviceSinkBuilder::open_default_sink()
|
||||
.expect("Can't open default rodio stream");
|
||||
let player = Arc::new(rodio::Player::connect_new(stream_handle.mixer()));
|
||||
(Arc::new(stream_handle), player)
|
||||
},
|
||||
scroll_id: Id::unique(),
|
||||
|
|
@ -231,14 +225,12 @@ impl Presenter {
|
|||
match message {
|
||||
Message::NextSlide => {
|
||||
if let Some((item, slide)) = self.next_slide() {
|
||||
return self
|
||||
.update(Message::ActivateSlide(item, slide));
|
||||
return self.update(Message::ActivateSlide(item, slide));
|
||||
}
|
||||
}
|
||||
Message::PrevSlide => {
|
||||
if let Some((item, slide)) = self.previous_slide() {
|
||||
return self
|
||||
.update(Message::ActivateSlide(item, slide));
|
||||
return self.update(Message::ActivateSlide(item, slide));
|
||||
}
|
||||
}
|
||||
Message::ActivateSlide(item_index, slide_index) => {
|
||||
|
|
@ -260,22 +252,15 @@ impl Presenter {
|
|||
self.obs_client = Some(client);
|
||||
}
|
||||
Message::RightClickSlide(item_index, slide_index) => {
|
||||
debug!(
|
||||
item_index,
|
||||
slide_index, "right clicked slide"
|
||||
);
|
||||
self.context_menu_id =
|
||||
Some((item_index, slide_index));
|
||||
self.context_point =
|
||||
self.hovered_point.unwrap_or_default();
|
||||
debug!(item_index, slide_index, "right clicked slide");
|
||||
self.context_menu_id = Some((item_index, slide_index));
|
||||
self.context_point = self.hovered_point.unwrap_or_default();
|
||||
if let Some(client) = &self.obs_client {
|
||||
let client = Arc::clone(client);
|
||||
return Action::Task(Task::perform(
|
||||
async move { client.scenes().list().await },
|
||||
|res| match res {
|
||||
Ok(scenes) => Message::UpdateObsScenes(
|
||||
scenes.scenes,
|
||||
),
|
||||
Ok(scenes) => Message::UpdateObsScenes(scenes.scenes),
|
||||
Err(_) => todo!(),
|
||||
},
|
||||
));
|
||||
|
|
@ -286,7 +271,9 @@ impl Presenter {
|
|||
self.obs_scenes = Some(scenes);
|
||||
}
|
||||
Message::AssignObsScene(scene_index) => {
|
||||
let slide_id = self.context_menu_id.expect("In this match we should always already have a context menu id");
|
||||
let slide_id = self.context_menu_id.expect(
|
||||
"In this match we should always already have a context menu id",
|
||||
);
|
||||
let Some(scenes) = &self.obs_scenes else {
|
||||
return Action::None;
|
||||
};
|
||||
|
|
@ -299,22 +286,16 @@ impl Presenter {
|
|||
match action {
|
||||
slide_actions::Action::Obs {
|
||||
action: ObsAction::Scene { .. },
|
||||
} => altered_actions.push(
|
||||
slide_actions::Action::Obs {
|
||||
action: ObsAction::Scene {
|
||||
scene: new_scene.clone(),
|
||||
},
|
||||
} => altered_actions.push(slide_actions::Action::Obs {
|
||||
action: ObsAction::Scene {
|
||||
scene: new_scene.clone(),
|
||||
},
|
||||
),
|
||||
_ => altered_actions
|
||||
.push(action.to_owned()),
|
||||
}),
|
||||
_ => altered_actions.push(action.to_owned()),
|
||||
}
|
||||
}
|
||||
*actions = altered_actions;
|
||||
debug!(
|
||||
"updating the obs scene {:?}",
|
||||
new_scene
|
||||
);
|
||||
debug!("updating the obs scene {:?}", new_scene);
|
||||
} else if map
|
||||
.insert(
|
||||
slide_id,
|
||||
|
|
@ -326,15 +307,9 @@ impl Presenter {
|
|||
)
|
||||
.is_none()
|
||||
{
|
||||
debug!(
|
||||
"adding the obs scene {:?}",
|
||||
new_scene
|
||||
);
|
||||
debug!("adding the obs scene {:?}", new_scene);
|
||||
} else {
|
||||
debug!(
|
||||
"updating the obs scene {:?}",
|
||||
new_scene
|
||||
);
|
||||
debug!("updating the obs scene {:?}", new_scene);
|
||||
}
|
||||
} else {
|
||||
let mut map = HashMap::new();
|
||||
|
|
@ -350,7 +325,9 @@ impl Presenter {
|
|||
}
|
||||
}
|
||||
Message::AssignSlideAction(action) => {
|
||||
let slide_id = self.context_menu_id.expect("In this match we should always already have a context menu id");
|
||||
let slide_id = self.context_menu_id.expect(
|
||||
"In this match we should always already have a context menu id",
|
||||
);
|
||||
if let Some(map) = self.slide_action_map.as_mut() {
|
||||
if let Some(actions) = map.get_mut(&slide_id) {
|
||||
actions.push(action);
|
||||
|
|
@ -395,40 +372,30 @@ impl Presenter {
|
|||
Message::StartVideo => {
|
||||
if let Some(video) = &mut self.preview_video {
|
||||
video.set_paused(false);
|
||||
video
|
||||
.set_looping(self.current_slide.video_loop());
|
||||
video.set_looping(self.current_slide.video_loop());
|
||||
}
|
||||
if let Some(video) = &mut self.presentation_video {
|
||||
video.set_paused(false);
|
||||
video
|
||||
.set_looping(self.current_slide.video_loop());
|
||||
video.set_looping(self.current_slide.video_loop());
|
||||
}
|
||||
}
|
||||
Message::PlayPauseVideo => {
|
||||
if let Some(video) = &mut self.preview_video {
|
||||
video.set_paused(!video.paused());
|
||||
video
|
||||
.set_looping(self.current_slide.video_loop());
|
||||
video.set_looping(self.current_slide.video_loop());
|
||||
}
|
||||
if let Some(video) = &mut self.presentation_video {
|
||||
video.set_paused(!video.paused());
|
||||
video
|
||||
.set_looping(self.current_slide.video_loop());
|
||||
video.set_looping(self.current_slide.video_loop());
|
||||
}
|
||||
}
|
||||
Message::VideoPos(position) => {
|
||||
if let Some(video) = &mut self.preview_video {
|
||||
let position = Position::Time(
|
||||
std::time::Duration::from_secs_f32(position),
|
||||
);
|
||||
let position =
|
||||
Position::Time(std::time::Duration::from_secs_f32(position));
|
||||
match video.seek(position, false) {
|
||||
Ok(()) => debug!(
|
||||
"Video position changed: {:?}",
|
||||
position
|
||||
),
|
||||
Err(e) => error!(
|
||||
"Problem changing video position: {e}"
|
||||
),
|
||||
Ok(()) => debug!("Video position changed: {:?}", position),
|
||||
Err(e) => error!("Problem changing video position: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -437,8 +404,7 @@ impl Presenter {
|
|||
&& self.video_position > 0.0
|
||||
&& video.position().as_secs_f32() != 0.0
|
||||
{
|
||||
self.video_position =
|
||||
video.position().as_secs_f32();
|
||||
self.video_position = video.position().as_secs_f32();
|
||||
}
|
||||
}
|
||||
Message::MissingPlugin(element) => {
|
||||
|
|
@ -525,40 +491,27 @@ impl Presenter {
|
|||
let mut items = vec![];
|
||||
for (item_index, item) in self.service.iter().enumerate() {
|
||||
let slides_length = item.slides.len();
|
||||
for (slide_index, slide) in item.slides.iter().enumerate()
|
||||
{
|
||||
for (slide_index, slide) in item.slides.iter().enumerate() {
|
||||
let is_current_slide = (item_index, slide_index)
|
||||
== (
|
||||
self.current_item_index,
|
||||
self.current_slide_index,
|
||||
);
|
||||
== (self.current_item_index, self.current_slide_index);
|
||||
|
||||
let slide = slide_view(
|
||||
slide,
|
||||
self.preview_video.as_ref(),
|
||||
true,
|
||||
false,
|
||||
);
|
||||
let slide = slide_view(slide, self.preview_video.as_ref(), true, false);
|
||||
let delegate = mouse_area(
|
||||
Container::new(slide)
|
||||
.style(move |t| {
|
||||
let mut style =
|
||||
container::Style::default();
|
||||
let mut style = container::Style::default();
|
||||
let theme = t.cosmic();
|
||||
let hovered = self.hovered_slide
|
||||
== Some((item_index, slide_index));
|
||||
let hovered =
|
||||
self.hovered_slide == Some((item_index, slide_index));
|
||||
style.background =
|
||||
Some(Background::Color(
|
||||
if is_current_slide {
|
||||
theme.accent.base.into()
|
||||
} else if hovered {
|
||||
theme.accent.hover.into()
|
||||
} else {
|
||||
theme.palette.neutral_3.into()
|
||||
},
|
||||
));
|
||||
style.border =
|
||||
Border::default().rounded(10.0);
|
||||
Some(Background::Color(if is_current_slide {
|
||||
theme.accent.base.into()
|
||||
} else if hovered {
|
||||
theme.accent.hover.into()
|
||||
} else {
|
||||
theme.palette.neutral_3.into()
|
||||
}));
|
||||
style.border = Border::default().rounded(10.0);
|
||||
style.shadow = Shadow {
|
||||
color: Color::BLACK,
|
||||
offset: {
|
||||
|
|
@ -582,41 +535,26 @@ impl Presenter {
|
|||
.height(self.preview_size)
|
||||
.padding(10),
|
||||
)
|
||||
.interaction(
|
||||
cosmic::iced::mouse::Interaction::Pointer,
|
||||
)
|
||||
.interaction(cosmic::iced::mouse::Interaction::Pointer)
|
||||
.on_move(move |point| {
|
||||
Message::HoveredSlide(Some((
|
||||
item_index,
|
||||
slide_index,
|
||||
point,
|
||||
)))
|
||||
Message::HoveredSlide(Some((item_index, slide_index, point)))
|
||||
})
|
||||
.on_exit(Message::HoveredSlide(None))
|
||||
.on_release(Message::ActivateSlide(
|
||||
item_index,
|
||||
slide_index,
|
||||
))
|
||||
.on_right_release(
|
||||
Message::RightClickSlide(item_index, slide_index),
|
||||
);
|
||||
.on_release(Message::ActivateSlide(item_index, slide_index))
|
||||
.on_right_release(Message::RightClickSlide(item_index, slide_index));
|
||||
let item = if slide_index == 0 {
|
||||
let label =
|
||||
text::body(elide_text(&item.title, 150.0));
|
||||
let label = text::body(elide_text(&item.title, 150.0));
|
||||
|
||||
column![label, delegate]
|
||||
.align_x(Horizontal::Center)
|
||||
.spacing(space_s)
|
||||
.apply(container)
|
||||
} else {
|
||||
delegate.apply(container).padding([
|
||||
space_l, space_none, space_none, space_none,
|
||||
])
|
||||
delegate
|
||||
.apply(container)
|
||||
.padding([space_l, space_none, space_none, space_none])
|
||||
};
|
||||
items.push(self.context_menu(
|
||||
(item_index, slide_index),
|
||||
item.into(),
|
||||
));
|
||||
items.push(self.context_menu((item_index, slide_index), item.into()));
|
||||
|
||||
items.push(
|
||||
container(space::vertical().width(space_s))
|
||||
|
|
@ -625,10 +563,7 @@ impl Presenter {
|
|||
} else {
|
||||
theme::Container::WindowBackground
|
||||
})
|
||||
.padding([
|
||||
space_none, space_xs, space_none,
|
||||
space_xs,
|
||||
])
|
||||
.padding([space_none, space_xs, space_none, space_xs])
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
|
@ -636,9 +571,7 @@ impl Presenter {
|
|||
let scrollable = scrollable(
|
||||
container(
|
||||
flex_row(items)
|
||||
.justify_content(Some(
|
||||
JustifyContent::SpaceEvenly,
|
||||
))
|
||||
.justify_content(Some(JustifyContent::SpaceEvenly))
|
||||
.align_items(cosmic::iced::Alignment::Center)
|
||||
.justify_items(cosmic::iced::Alignment::End)
|
||||
.column_spacing(space_s)
|
||||
|
|
@ -662,71 +595,47 @@ impl Presenter {
|
|||
#[allow(clippy::too_many_lines)]
|
||||
pub fn preview_bar(&self) -> Element<Message> {
|
||||
let mut items = vec![];
|
||||
self.service.iter().enumerate().for_each(
|
||||
|(item_index, item)| {
|
||||
self.service
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(item_index, item)| {
|
||||
let mut slides = vec![];
|
||||
item.slides.iter().enumerate().for_each(
|
||||
|(slide_index, slide)| {
|
||||
let is_current_slide =
|
||||
(item_index, slide_index)
|
||||
== (
|
||||
self.current_item_index,
|
||||
self.current_slide_index,
|
||||
);
|
||||
item.slides
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(slide_index, slide)| {
|
||||
let is_current_slide = (item_index, slide_index)
|
||||
== (self.current_item_index, self.current_slide_index);
|
||||
|
||||
let container = slide_view(
|
||||
slide,
|
||||
self.preview_video.as_ref(),
|
||||
true,
|
||||
false,
|
||||
);
|
||||
let container =
|
||||
slide_view(slide, self.preview_video.as_ref(), true, false);
|
||||
let delegate = mouse_area(
|
||||
Container::new(container)
|
||||
.style(move |t| {
|
||||
let mut style =
|
||||
container::Style::default();
|
||||
let mut style = container::Style::default();
|
||||
let theme = t.cosmic();
|
||||
let hovered = self.hovered_slide
|
||||
== Some((
|
||||
item_index,
|
||||
slide_index,
|
||||
));
|
||||
== Some((item_index, slide_index));
|
||||
style.background =
|
||||
Some(Background::Color(
|
||||
if is_current_slide {
|
||||
theme
|
||||
.accent
|
||||
.base
|
||||
.into()
|
||||
} else if hovered {
|
||||
theme
|
||||
.accent
|
||||
.hover
|
||||
.into()
|
||||
} else {
|
||||
theme
|
||||
.palette
|
||||
.neutral_3
|
||||
.into()
|
||||
},
|
||||
));
|
||||
style.border = Border::default()
|
||||
.rounded(10.0);
|
||||
Some(Background::Color(if is_current_slide {
|
||||
theme.accent.base.into()
|
||||
} else if hovered {
|
||||
theme.accent.hover.into()
|
||||
} else {
|
||||
theme.palette.neutral_3.into()
|
||||
}));
|
||||
style.border = Border::default().rounded(10.0);
|
||||
style.shadow = Shadow {
|
||||
color: Color::BLACK,
|
||||
offset: {
|
||||
if is_current_slide
|
||||
|| hovered
|
||||
{
|
||||
if is_current_slide || hovered {
|
||||
Vector::new(5.0, 5.0)
|
||||
} else {
|
||||
Vector::new(0.0, 0.0)
|
||||
}
|
||||
},
|
||||
blur_radius: {
|
||||
if is_current_slide
|
||||
|| hovered
|
||||
{
|
||||
if is_current_slide || hovered {
|
||||
10.0
|
||||
} else {
|
||||
0.0
|
||||
|
|
@ -735,41 +644,24 @@ impl Presenter {
|
|||
};
|
||||
style
|
||||
})
|
||||
.center_x(
|
||||
self.preview_size * 16.0 / 9.0,
|
||||
)
|
||||
.center_x(self.preview_size * 16.0 / 9.0)
|
||||
.height(self.preview_size)
|
||||
.padding(10),
|
||||
)
|
||||
.interaction(
|
||||
cosmic::iced::mouse::Interaction::Pointer,
|
||||
)
|
||||
.interaction(cosmic::iced::mouse::Interaction::Pointer)
|
||||
.on_move(move |point| {
|
||||
Message::HoveredSlide(Some((
|
||||
item_index,
|
||||
slide_index,
|
||||
point,
|
||||
)))
|
||||
Message::HoveredSlide(Some((item_index, slide_index, point)))
|
||||
})
|
||||
.on_exit(Message::HoveredSlide(None))
|
||||
.on_release(Message::ActivateSlide(
|
||||
item_index,
|
||||
slide_index,
|
||||
))
|
||||
.on_right_release(Message::RightClickSlide(
|
||||
item_index,
|
||||
slide_index,
|
||||
));
|
||||
let context_menu = self.context_menu(
|
||||
(item_index, slide_index),
|
||||
delegate.into(),
|
||||
.on_release(Message::ActivateSlide(item_index, slide_index))
|
||||
.on_right_release(
|
||||
Message::RightClickSlide(item_index, slide_index),
|
||||
);
|
||||
let context_menu =
|
||||
self.context_menu((item_index, slide_index), delegate.into());
|
||||
slides.push(context_menu);
|
||||
},
|
||||
);
|
||||
let row = Row::from_vec(slides)
|
||||
.spacing(10)
|
||||
.padding([20, 15]);
|
||||
});
|
||||
let row = Row::from_vec(slides).spacing(10).padding([20, 15]);
|
||||
let label = text::body(item.title.clone());
|
||||
let label_container = container(label)
|
||||
.align_top(Length::Fill)
|
||||
|
|
@ -782,17 +674,15 @@ impl Presenter {
|
|||
.into(),
|
||||
);
|
||||
items.push(divider.into());
|
||||
},
|
||||
);
|
||||
let scrollable =
|
||||
scrollable(container(Row::from_vec(items)).style(|_t| {
|
||||
let style = container::Style::default();
|
||||
style.border(Border::default().width(2))
|
||||
}))
|
||||
.direction(Direction::Horizontal(Scrollbar::new()))
|
||||
.height(Length::Fill)
|
||||
// .width(Length::Fill)
|
||||
.id(self.scroll_id.clone());
|
||||
});
|
||||
let scrollable = scrollable(container(Row::from_vec(items)).style(|_t| {
|
||||
let style = container::Style::default();
|
||||
style.border(Border::default().width(2))
|
||||
}))
|
||||
.direction(Direction::Horizontal(Scrollbar::new()))
|
||||
.height(Length::Fill)
|
||||
// .width(Length::Fill)
|
||||
.id(self.scroll_id.clone());
|
||||
scrollable.into()
|
||||
}
|
||||
|
||||
|
|
@ -806,11 +696,8 @@ impl Presenter {
|
|||
.is_some_and(|context_id| context_id == id)
|
||||
{
|
||||
let menu_item = |label, message| {
|
||||
menu::menu_button(vec![
|
||||
text(label).into(),
|
||||
space::horizontal().into(),
|
||||
])
|
||||
.on_press(message)
|
||||
menu::menu_button(vec![text(label).into(), space::horizontal().into()])
|
||||
.on_press(message)
|
||||
};
|
||||
let obs_scenes = self.obs_scenes.as_ref().map(|scenes| {
|
||||
scenes.iter().map(|scene| {
|
||||
|
|
@ -824,20 +711,16 @@ impl Presenter {
|
|||
let mut menu_items: Vec<Element<Message>> = vec![
|
||||
menu_item(
|
||||
"Start Stream",
|
||||
Message::AssignSlideAction(
|
||||
slide_actions::Action::Obs {
|
||||
action: ObsAction::StartStream,
|
||||
},
|
||||
),
|
||||
Message::AssignSlideAction(slide_actions::Action::Obs {
|
||||
action: ObsAction::StartStream,
|
||||
}),
|
||||
)
|
||||
.into(),
|
||||
menu_item(
|
||||
"Stop Stream",
|
||||
Message::AssignSlideAction(
|
||||
slide_actions::Action::Obs {
|
||||
action: ObsAction::StopStream,
|
||||
},
|
||||
),
|
||||
Message::AssignSlideAction(slide_actions::Action::Obs {
|
||||
action: ObsAction::StopStream,
|
||||
}),
|
||||
)
|
||||
.into(),
|
||||
];
|
||||
|
|
@ -846,11 +729,7 @@ impl Presenter {
|
|||
menu_items.push(
|
||||
text("Scenes")
|
||||
.class(theme::Text::Color(
|
||||
theme::active()
|
||||
.cosmic()
|
||||
.palette
|
||||
.neutral_7
|
||||
.into(),
|
||||
theme::active().cosmic().palette.neutral_7.into(),
|
||||
))
|
||||
.align_x(Alignment::Center)
|
||||
.apply(container)
|
||||
|
|
@ -871,9 +750,7 @@ impl Presenter {
|
|||
.class(theme::Container::Dropdown);
|
||||
|
||||
popover(items)
|
||||
.position(popover::Position::Point(
|
||||
self.context_point,
|
||||
))
|
||||
.position(popover::Position::Point(self.context_point))
|
||||
.on_close(Message::CloseContextMenu)
|
||||
.popup(item_column)
|
||||
.into()
|
||||
|
|
@ -883,35 +760,39 @@ impl Presenter {
|
|||
}
|
||||
|
||||
fn reset_video(&mut self) {
|
||||
if self.current_slide.background().kind
|
||||
== BackgroundKind::Video
|
||||
if self.current_slide.background().kind == BackgroundKind::Video
|
||||
&& let path = &self.current_slide.background().path
|
||||
&& path.exists()
|
||||
{
|
||||
let url = Url::from_file_path(path)
|
||||
.expect("There should be a video file here");
|
||||
match gst_video::create_video(&url, 15) {
|
||||
let url =
|
||||
Url::from_file_path(path).expect("There should be a video file here");
|
||||
|
||||
let video_settings = VideoSettings {
|
||||
mute: true,
|
||||
framerate: 15,
|
||||
};
|
||||
match gst_video::create_video(&url, &video_settings) {
|
||||
Ok(mut v) => {
|
||||
v.set_looping(self.current_slide.video_loop());
|
||||
v.set_muted(true);
|
||||
self.preview_video = Some(v);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Had an error creating the video object: {e}"
|
||||
);
|
||||
error!("Had an error creating the video object: {e}");
|
||||
self.preview_video = None;
|
||||
}
|
||||
}
|
||||
match gst_video::create_video(&url, 60) {
|
||||
let video_settings = VideoSettings {
|
||||
mute: false,
|
||||
framerate: 60,
|
||||
};
|
||||
match gst_video::create_video(&url, &video_settings) {
|
||||
Ok(mut v) => {
|
||||
v.set_looping(self.current_slide.video_loop());
|
||||
self.presentation_video = Some(v);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Had an error creating the video object: {e}"
|
||||
);
|
||||
error!("Had an error creating the video object: {e}");
|
||||
self.presentation_video = None;
|
||||
}
|
||||
}
|
||||
|
|
@ -922,8 +803,7 @@ impl Presenter {
|
|||
}
|
||||
|
||||
pub fn update_items(&mut self, items: Arc<Vec<ServiceItem>>) {
|
||||
let total_slides: usize =
|
||||
items.iter().fold(0, |a, item| a + item.slides.len());
|
||||
let total_slides: usize = items.iter().fold(0, |a, item| a + item.slides.len());
|
||||
self.service = items;
|
||||
self.total_slides = total_slides;
|
||||
}
|
||||
|
|
@ -932,10 +812,8 @@ impl Presenter {
|
|||
let mut tasks = vec![];
|
||||
|
||||
if let Some(map) = &self.slide_action_map
|
||||
&& let Some(actions) = map.get(&(
|
||||
self.current_item_index,
|
||||
self.current_slide_index,
|
||||
))
|
||||
&& let Some(actions) =
|
||||
map.get(&(self.current_item_index, self.current_slide_index))
|
||||
{
|
||||
for action in actions {
|
||||
match action {
|
||||
|
|
@ -963,10 +841,7 @@ impl Presenter {
|
|||
|
||||
#[allow(unused)]
|
||||
fn load_images(&mut self) {
|
||||
if matches!(
|
||||
self.current_slide.background.kind,
|
||||
BackgroundKind::Image
|
||||
) {
|
||||
if matches!(self.current_slide.background.kind, BackgroundKind::Image) {
|
||||
let path = &self.current_slide.background().path;
|
||||
self.current_slide.background.image_handle =
|
||||
match self.image_loader.get_image(path) {
|
||||
|
|
@ -979,10 +854,7 @@ impl Presenter {
|
|||
}
|
||||
|
||||
if let Some(slide) = self.activating_slide.as_mut()
|
||||
&& matches!(
|
||||
slide.background().kind,
|
||||
BackgroundKind::Image
|
||||
)
|
||||
&& matches!(slide.background().kind, BackgroundKind::Image)
|
||||
{
|
||||
let path = &slide.background().path;
|
||||
let res_handle = self.image_loader.load_image(path);
|
||||
|
|
@ -996,13 +868,13 @@ impl Presenter {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn change_slide(&mut self, slide: Slide) -> Action {
|
||||
let slide_text = slide.text();
|
||||
debug!(slide_text, "slide changed");
|
||||
let bg = slide.background().clone();
|
||||
debug!(?bg, "comparing background...");
|
||||
let backgrounds_match =
|
||||
self.current_slide.background() == slide.background();
|
||||
let backgrounds_match = self.current_slide.background() == slide.background();
|
||||
|
||||
debug!("cloning slide...");
|
||||
let font = slide
|
||||
|
|
@ -1024,25 +896,24 @@ impl Presenter {
|
|||
|
||||
let mut target_item = 0;
|
||||
|
||||
self.service.iter().enumerate().try_for_each(
|
||||
|(index, item)| {
|
||||
item.slides.iter().enumerate().try_for_each(
|
||||
|(slide_index, _)| {
|
||||
self.service
|
||||
.iter()
|
||||
.enumerate()
|
||||
.try_for_each(|(index, item)| {
|
||||
item.slides
|
||||
.iter()
|
||||
.enumerate()
|
||||
.try_for_each(|(slide_index, _)| {
|
||||
target_item += 1;
|
||||
if (index, slide_index)
|
||||
== (
|
||||
self.current_item_index,
|
||||
self.current_slide_index,
|
||||
)
|
||||
== (self.current_item_index, self.current_slide_index)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(())
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
debug!(target_item);
|
||||
|
||||
|
|
@ -1075,25 +946,23 @@ impl Presenter {
|
|||
&& audio.exists()
|
||||
{
|
||||
let file = BufReader::new(
|
||||
File::open(audio)
|
||||
.expect("There should be an audio file here"),
|
||||
File::open(audio).expect("There should be an audio file here"),
|
||||
);
|
||||
let source = Decoder::new(file)
|
||||
.expect("There should be an audio decoder here");
|
||||
let source =
|
||||
Decoder::new(file).expect("There should be an audio decoder here");
|
||||
self.audio_duration = source.total_duration();
|
||||
self.sink.1.append(source);
|
||||
self.sink.1.play();
|
||||
self.audio_position = Some(self.sink.1.get_pos());
|
||||
}
|
||||
}
|
||||
if self.service.get(self.current_item_index).is_some_and(
|
||||
|item| {
|
||||
matches!(
|
||||
item.kind,
|
||||
crate::core::kinds::ServiceItemKind::Song(..)
|
||||
)
|
||||
},
|
||||
) {
|
||||
if self
|
||||
.service
|
||||
.get(self.current_item_index)
|
||||
.is_some_and(|item| {
|
||||
matches!(item.kind, crate::core::kinds::ServiceItemKind::Song(..))
|
||||
})
|
||||
{
|
||||
self.animation = Some(
|
||||
Animation::new(false)
|
||||
.slow()
|
||||
|
|
@ -1116,9 +985,7 @@ impl Presenter {
|
|||
}
|
||||
|
||||
pub(crate) fn previous_slide(&self) -> Option<(usize, usize)> {
|
||||
if self.current_item_index == 0
|
||||
&& self.current_slide_index == 0
|
||||
{
|
||||
if self.current_item_index == 0 && self.current_slide_index == 0 {
|
||||
return None;
|
||||
} else if self.current_slide_index == 0 {
|
||||
let target_item = self.current_item_index - 1;
|
||||
|
|
@ -1126,9 +993,7 @@ impl Presenter {
|
|||
.service
|
||||
.get(target_item)
|
||||
.map(|i| i.slides.len() - 1)
|
||||
.expect(
|
||||
"We have checked that this item should be here.",
|
||||
);
|
||||
.expect("We have checked that this item should be here.");
|
||||
return Some((target_item, last_slide));
|
||||
}
|
||||
let target_slide = self.current_slide_index - 1;
|
||||
|
|
@ -1136,21 +1001,18 @@ impl Presenter {
|
|||
}
|
||||
|
||||
pub(crate) fn next_slide(&self) -> Option<(usize, usize)> {
|
||||
if self.service.get(self.current_item_index).is_some_and(
|
||||
|i| i.slides.len() == self.current_slide_index + 1,
|
||||
) {
|
||||
if self
|
||||
.service
|
||||
.get(self.current_item_index)
|
||||
.is_some_and(|i| i.slides.len() == self.current_slide_index + 1)
|
||||
{
|
||||
return Some((self.current_item_index + 1, 0));
|
||||
} else if self
|
||||
.service
|
||||
.get(self.current_item_index)
|
||||
.is_some_and(|i| {
|
||||
i.slides.len() > self.current_slide_index + 1
|
||||
})
|
||||
.is_some_and(|i| i.slides.len() > self.current_slide_index + 1)
|
||||
{
|
||||
return Some((
|
||||
self.current_item_index,
|
||||
self.current_slide_index + 1,
|
||||
));
|
||||
return Some((self.current_item_index, self.current_slide_index + 1));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -1166,9 +1028,7 @@ pub(crate) fn slide_view<'a>(
|
|||
responsive(move |size| {
|
||||
let width = size.height * 16.0 / 9.0;
|
||||
let black = Container::new(Space::new())
|
||||
.style(|_| {
|
||||
container::background(Background::Color(Color::BLACK))
|
||||
})
|
||||
.style(|_| container::background(Background::Color(Color::BLACK)))
|
||||
.clip(true)
|
||||
.width(width)
|
||||
.height(Length::Fill);
|
||||
|
|
@ -1176,40 +1036,30 @@ pub(crate) fn slide_view<'a>(
|
|||
|
||||
match slide.background().kind {
|
||||
BackgroundKind::Image => {
|
||||
stack = stack.push(
|
||||
slide
|
||||
.background()
|
||||
.image_handle
|
||||
.as_ref()
|
||||
.map_or_else(
|
||||
|| {
|
||||
Container::new(
|
||||
cosmic_image(
|
||||
&slide.background().path,
|
||||
)
|
||||
.content_fit(ContentFit::Contain)
|
||||
.width(width)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.center(Length::Fill)
|
||||
.clip(true)
|
||||
},
|
||||
|handle| {
|
||||
Container::new(loaded_image(
|
||||
handle.clone(),
|
||||
cosmic_image(handle)
|
||||
.content_fit(
|
||||
ContentFit::Contain,
|
||||
)
|
||||
.width(width)
|
||||
.height(Length::Fill)
|
||||
.into(),
|
||||
))
|
||||
.center(Length::Fill)
|
||||
.clip(true)
|
||||
},
|
||||
),
|
||||
);
|
||||
stack = stack.push(slide.background().image_handle.as_ref().map_or_else(
|
||||
|| {
|
||||
Container::new(
|
||||
cosmic_image(&slide.background().path)
|
||||
.content_fit(ContentFit::Contain)
|
||||
.width(width)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.center(Length::Fill)
|
||||
.clip(true)
|
||||
},
|
||||
|handle| {
|
||||
Container::new(loaded_image(
|
||||
handle.clone(),
|
||||
cosmic_image(handle)
|
||||
.content_fit(ContentFit::Contain)
|
||||
.width(width)
|
||||
.height(Length::Fill)
|
||||
.into(),
|
||||
))
|
||||
.center(Length::Fill)
|
||||
.clip(true)
|
||||
},
|
||||
));
|
||||
}
|
||||
BackgroundKind::Video => {
|
||||
if let Some(video) = &video
|
||||
|
|
@ -1223,15 +1073,9 @@ pub(crate) fn slide_view<'a>(
|
|||
.height(Length::Fill)
|
||||
.on_end_of_stream(Message::EndVideo)
|
||||
.on_new_frame(Message::VideoFrame)
|
||||
.on_missing_plugin(
|
||||
Message::MissingPlugin,
|
||||
)
|
||||
.on_warning(|w| {
|
||||
Message::Error(w.to_string())
|
||||
})
|
||||
.on_error(|e| {
|
||||
Message::Error(e.to_string())
|
||||
})
|
||||
.on_missing_plugin(Message::MissingPlugin)
|
||||
.on_warning(|w| Message::Error(w.to_string()))
|
||||
.on_error(|e| Message::Error(e.to_string()))
|
||||
.content_fit(ContentFit::Contain),
|
||||
)
|
||||
.center(Length::Fill)
|
||||
|
|
|
|||
|
|
@ -55,10 +55,7 @@ struct EditorProgram {
|
|||
}
|
||||
|
||||
impl SlideEditor {
|
||||
pub fn view(
|
||||
&self,
|
||||
_font: Font,
|
||||
) -> cosmic::Element<'_, SlideWidget> {
|
||||
pub fn view(&self, _font: Font) -> cosmic::Element<'_, SlideWidget> {
|
||||
container(
|
||||
widget::canvas(&self.program)
|
||||
.height(Length::Fill)
|
||||
|
|
@ -71,9 +68,7 @@ impl SlideEditor {
|
|||
/// Ensure to use the `cosmic::Theme and cosmic::Renderer` here
|
||||
/// or else it will not compile
|
||||
#[allow(clippy::extra_unused_lifetimes)]
|
||||
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
||||
for EditorProgram
|
||||
{
|
||||
impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer> for EditorProgram {
|
||||
type State = ();
|
||||
|
||||
fn draw(
|
||||
|
|
@ -99,15 +94,11 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
|||
frame.fill(&circle, Color::BLACK);
|
||||
frame.stroke(
|
||||
&circle,
|
||||
Stroke::default()
|
||||
.with_width(5.0)
|
||||
.with_color(Color::BLACK),
|
||||
Stroke::default().with_width(5.0).with_color(Color::BLACK),
|
||||
);
|
||||
frame.stroke(
|
||||
&border,
|
||||
Stroke::default()
|
||||
.with_width(5.0)
|
||||
.with_color(Color::BLACK),
|
||||
Stroke::default().with_width(5.0).with_color(Color::BLACK),
|
||||
);
|
||||
|
||||
// Then, we produce the geometry
|
||||
|
|
@ -129,9 +120,7 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
|||
cosmic::iced::mouse::Event::CursorLeft => {
|
||||
debug!("cursor left");
|
||||
}
|
||||
cosmic::iced::mouse::Event::CursorMoved {
|
||||
position,
|
||||
} => {
|
||||
cosmic::iced::mouse::Event::CursorMoved { position } => {
|
||||
if bounds.x < position.x
|
||||
&& bounds.y < position.y
|
||||
&& (bounds.width + bounds.x) > position.x
|
||||
|
|
@ -144,12 +133,12 @@ impl<'a> Program<SlideWidget, cosmic::Theme, cosmic::Renderer>
|
|||
// self.mouse_button_pressed = Some(button);
|
||||
debug!(?button, "mouse button pressed");
|
||||
}
|
||||
cosmic::iced::mouse::Event::ButtonReleased(
|
||||
button,
|
||||
) => debug!(?button, "mouse button released"),
|
||||
cosmic::iced::mouse::Event::WheelScrolled {
|
||||
delta,
|
||||
} => debug!(?delta, "scroll wheel"),
|
||||
cosmic::iced::mouse::Event::ButtonReleased(button) => {
|
||||
debug!(?button, "mouse button released")
|
||||
}
|
||||
cosmic::iced::mouse::Event::WheelScrolled { delta } => {
|
||||
debug!(?delta, "scroll wheel")
|
||||
}
|
||||
},
|
||||
canvas::Event::Touch(_event) => debug!("test"),
|
||||
canvas::Event::Keyboard(_event) => debug!("test"),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -63,9 +63,7 @@ impl Hash for TextSvg {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Font {
|
||||
name: String,
|
||||
weight: Weight,
|
||||
|
|
@ -73,9 +71,7 @@ pub struct Font {
|
|||
size: u8,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct Shadow {
|
||||
pub offset_x: i16,
|
||||
pub offset_y: i16,
|
||||
|
|
@ -83,9 +79,7 @@ pub struct Shadow {
|
|||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct Stroke {
|
||||
size: u16,
|
||||
color: Color,
|
||||
|
|
@ -102,9 +96,7 @@ impl From<cosmic::font::Font> for Font {
|
|||
fn from(value: cosmic::font::Font) -> Self {
|
||||
Self {
|
||||
name: match value.family {
|
||||
cosmic::iced::font::Family::Name(name) => {
|
||||
name.to_string()
|
||||
}
|
||||
cosmic::iced::font::Family::Name(name) => name.to_string(),
|
||||
_ => "Quicksand Bold".into(),
|
||||
},
|
||||
size: 20,
|
||||
|
|
@ -230,10 +222,7 @@ impl Default for Color {
|
|||
}
|
||||
|
||||
impl Display for Color {
|
||||
fn fmt(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_css_hex_string())
|
||||
}
|
||||
}
|
||||
|
|
@ -286,10 +275,7 @@ impl TextSvg {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn alignment(
|
||||
mut self,
|
||||
alignment: TextAlignment,
|
||||
) -> Self {
|
||||
pub const fn alignment(mut self, alignment: TextAlignment) -> Self {
|
||||
self.alignment = alignment;
|
||||
self
|
||||
}
|
||||
|
|
@ -297,11 +283,7 @@ impl TextSvg {
|
|||
#[must_use]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn build(
|
||||
mut self,
|
||||
size: Size,
|
||||
mut cache: Option<PathBuf>,
|
||||
) -> Self {
|
||||
pub fn build(mut self, size: Size, mut cache: Option<PathBuf>) -> Self {
|
||||
// debug!("starting...");
|
||||
|
||||
let mut final_svg = String::with_capacity(1024);
|
||||
|
|
@ -316,55 +298,47 @@ impl TextSvg {
|
|||
let center_y = (size.width / 2.0).to_string();
|
||||
let x_width_padded = (size.width - 10.0).to_string();
|
||||
|
||||
let (text_anchor, starting_y_position, text_x_position) =
|
||||
match self.alignment {
|
||||
TextAlignment::TopLeft => ("start", font_size, "10"),
|
||||
TextAlignment::TopCenter => {
|
||||
("middle", font_size, center_y.as_str())
|
||||
}
|
||||
TextAlignment::TopRight => {
|
||||
("end", font_size, x_width_padded.as_str())
|
||||
}
|
||||
TextAlignment::MiddleLeft => {
|
||||
let middle_position = size.height / 2.0;
|
||||
let position = half_lines.mul_add(
|
||||
-text_and_line_spacing,
|
||||
middle_position,
|
||||
) + text_and_line_spacing / 2.0;
|
||||
("start", position, "10")
|
||||
}
|
||||
TextAlignment::MiddleCenter => {
|
||||
let middle_position = size.height / 2.0;
|
||||
let position = half_lines.mul_add(
|
||||
-text_and_line_spacing,
|
||||
middle_position,
|
||||
) + text_and_line_spacing / 2.0;
|
||||
("middle", position, center_y.as_str())
|
||||
}
|
||||
TextAlignment::MiddleRight => {
|
||||
let middle_position = size.height / 2.0;
|
||||
let position = half_lines.mul_add(
|
||||
-text_and_line_spacing,
|
||||
middle_position,
|
||||
) + text_and_line_spacing / 2.0;
|
||||
("end", position, x_width_padded.as_str())
|
||||
}
|
||||
TextAlignment::BottomLeft => {
|
||||
let position = (total_lines as f32)
|
||||
.mul_add(-text_and_line_spacing, size.height);
|
||||
("start", position, "10")
|
||||
}
|
||||
TextAlignment::BottomCenter => {
|
||||
let position = (total_lines as f32)
|
||||
.mul_add(-text_and_line_spacing, size.height);
|
||||
("middle", position, center_y.as_str())
|
||||
}
|
||||
TextAlignment::BottomRight => {
|
||||
let position = (total_lines as f32)
|
||||
.mul_add(-text_and_line_spacing, size.height);
|
||||
("end", position, x_width_padded.as_str())
|
||||
}
|
||||
};
|
||||
let (text_anchor, starting_y_position, text_x_position) = match self.alignment {
|
||||
TextAlignment::TopLeft => ("start", font_size, "10"),
|
||||
TextAlignment::TopCenter => ("middle", font_size, center_y.as_str()),
|
||||
TextAlignment::TopRight => ("end", font_size, x_width_padded.as_str()),
|
||||
TextAlignment::MiddleLeft => {
|
||||
let middle_position = size.height / 2.0;
|
||||
let position = half_lines
|
||||
.mul_add(-text_and_line_spacing, middle_position)
|
||||
+ text_and_line_spacing / 2.0;
|
||||
("start", position, "10")
|
||||
}
|
||||
TextAlignment::MiddleCenter => {
|
||||
let middle_position = size.height / 2.0;
|
||||
let position = half_lines
|
||||
.mul_add(-text_and_line_spacing, middle_position)
|
||||
+ text_and_line_spacing / 2.0;
|
||||
("middle", position, center_y.as_str())
|
||||
}
|
||||
TextAlignment::MiddleRight => {
|
||||
let middle_position = size.height / 2.0;
|
||||
let position = half_lines
|
||||
.mul_add(-text_and_line_spacing, middle_position)
|
||||
+ text_and_line_spacing / 2.0;
|
||||
("end", position, x_width_padded.as_str())
|
||||
}
|
||||
TextAlignment::BottomLeft => {
|
||||
let position =
|
||||
(total_lines as f32).mul_add(-text_and_line_spacing, size.height);
|
||||
("start", position, "10")
|
||||
}
|
||||
TextAlignment::BottomCenter => {
|
||||
let position =
|
||||
(total_lines as f32).mul_add(-text_and_line_spacing, size.height);
|
||||
("middle", position, center_y.as_str())
|
||||
}
|
||||
TextAlignment::BottomRight => {
|
||||
let position =
|
||||
(total_lines as f32).mul_add(-text_and_line_spacing, size.height);
|
||||
("end", position, x_width_padded.as_str())
|
||||
}
|
||||
};
|
||||
|
||||
let font_style = match self.font.style {
|
||||
Style::Normal => "normal",
|
||||
|
|
@ -373,9 +347,7 @@ impl TextSvg {
|
|||
};
|
||||
|
||||
let font_weight = match self.font.weight {
|
||||
Weight::Thin | Weight::ExtraLight | Weight::Light => {
|
||||
"lighter"
|
||||
}
|
||||
Weight::Thin | Weight::ExtraLight | Weight::Light => "lighter",
|
||||
Weight::Normal | Weight::Medium => "normal",
|
||||
Weight::Semibold | Weight::Bold => "bold",
|
||||
Weight::ExtraBold | Weight::Black => "bolder",
|
||||
|
|
@ -391,10 +363,7 @@ impl TextSvg {
|
|||
let _ = write!(
|
||||
final_svg,
|
||||
"<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
||||
shadow.offset_x,
|
||||
shadow.offset_y,
|
||||
shadow.spread,
|
||||
shadow.color
|
||||
shadow.offset_x, shadow.offset_y, shadow.spread, shadow.color
|
||||
);
|
||||
}
|
||||
final_svg.push_str("</defs>");
|
||||
|
|
@ -433,10 +402,7 @@ impl TextSvg {
|
|||
let _ = write!(
|
||||
final_svg,
|
||||
"<tspan x=\"0\" y=\"{}\">{}</tspan>",
|
||||
(index as f32).mul_add(
|
||||
text_and_line_spacing,
|
||||
starting_y_position
|
||||
),
|
||||
(index as f32).mul_add(text_and_line_spacing, starting_y_position),
|
||||
text
|
||||
);
|
||||
}
|
||||
|
|
@ -483,11 +449,9 @@ impl TextSvg {
|
|||
let transform = tiny_skia::Transform::default();
|
||||
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
let (size_width, size_height) =
|
||||
(size.width as u32, size.height as u32);
|
||||
let (size_width, size_height) = (size.width as u32, size.height as u32);
|
||||
|
||||
let Some(mut pixmap) = Pixmap::new(size_width, size_height)
|
||||
else {
|
||||
let Some(mut pixmap) = Pixmap::new(size_width, size_height) else {
|
||||
error!("Couldn't create a new pixmap from size");
|
||||
return self;
|
||||
};
|
||||
|
|
@ -503,8 +467,7 @@ impl TextSvg {
|
|||
|
||||
// debug!("saved");
|
||||
// let handle = Handle::from_path(path);
|
||||
let handle =
|
||||
Handle::from_rgba(size_width, size_height, pixmap.take());
|
||||
let handle = Handle::from_rgba(size_width, size_height, pixmap.take());
|
||||
self.handle = Some(handle);
|
||||
// debug!("stored");
|
||||
self
|
||||
|
|
@ -512,13 +475,7 @@ impl TextSvg {
|
|||
|
||||
pub fn view<'a>(&self) -> Element<'a, Message> {
|
||||
self.handle.clone().map_or_else(
|
||||
|| {
|
||||
Element::from(
|
||||
Space::new()
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
},
|
||||
|| Element::from(Space::new().height(Length::Fill).width(Length::Fill)),
|
||||
|handle| {
|
||||
Image::new(handle)
|
||||
.content_fit(ContentFit::Cover)
|
||||
|
|
@ -581,9 +538,7 @@ pub fn text_svg_generator_with_cache(
|
|||
let font = slide.font().unwrap_or_default();
|
||||
let text_svg = TextSvg::new(slide.text())
|
||||
.alignment(slide.text_alignment())
|
||||
.fill(
|
||||
slide.text_color().unwrap_or_else(|| "#fff".into()),
|
||||
);
|
||||
.fill(slide.text_color().unwrap_or_else(|| "#fff".into()));
|
||||
let text_svg = if let Some(stroke) = slide.stroke() {
|
||||
text_svg.stroke(stroke)
|
||||
} else {
|
||||
|
|
@ -596,8 +551,7 @@ pub fn text_svg_generator_with_cache(
|
|||
};
|
||||
let text_svg = text_svg.font(font).fontdb(Arc::clone(fontdb));
|
||||
// debug!(fill = ?text_svg.fill, font = ?text_svg.font, stroke = ?text_svg.stroke, shadow = ?text_svg.shadow, text = ?text_svg.text);
|
||||
let text_svg =
|
||||
text_svg.build(Size::new(1280.0, 720.0), cache);
|
||||
let text_svg = text_svg.build(Size::new(1280.0, 720.0), cache);
|
||||
slide.text_svg = Some(text_svg);
|
||||
Ok(slide)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ use cosmic::iced::Length;
|
|||
use cosmic::iced::alignment::Vertical;
|
||||
use cosmic::iced::widget::{column, row};
|
||||
use cosmic::widget::space::{self, horizontal};
|
||||
use cosmic::widget::{
|
||||
Space, button, container, icon, slider, text, text_input,
|
||||
};
|
||||
use cosmic::widget::{Space, button, container, icon, slider, text, text_input};
|
||||
use cosmic::{Element, Task, theme};
|
||||
use iced_video_player::{Position, Video, VideoPlayer};
|
||||
use tracing::{debug, error, warn};
|
||||
|
|
@ -89,9 +87,8 @@ impl VideoEditor {
|
|||
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),
|
||||
);
|
||||
let position =
|
||||
Position::Time(std::time::Duration::from_secs_f64(position));
|
||||
if let Err(e) = video.seek(position, false) {
|
||||
error!(?e);
|
||||
}
|
||||
|
|
@ -99,22 +96,14 @@ impl VideoEditor {
|
|||
}
|
||||
}
|
||||
Message::PickVideo => {
|
||||
let video_id = self
|
||||
.core_video
|
||||
.as_ref()
|
||||
.map(|v| v.id)
|
||||
.unwrap_or_default();
|
||||
let task = Task::perform(
|
||||
pick_video(),
|
||||
move |video_result| {
|
||||
video_result.map_or(Message::None, |video| {
|
||||
let mut video =
|
||||
videos::Video::from(video);
|
||||
video.id = video_id;
|
||||
Message::UpdateVideoFile(video)
|
||||
})
|
||||
},
|
||||
);
|
||||
let video_id = self.core_video.as_ref().map(|v| v.id).unwrap_or_default();
|
||||
let task = Task::perform(pick_video(), move |video_result| {
|
||||
video_result.map_or(Message::None, |video| {
|
||||
let mut video = videos::Video::from(video);
|
||||
video.id = video_id;
|
||||
Message::UpdateVideoFile(video)
|
||||
})
|
||||
});
|
||||
return Action::Task(task);
|
||||
}
|
||||
Message::UpdateVideoFile(video) => {
|
||||
|
|
@ -165,10 +154,7 @@ impl VideoEditor {
|
|||
let video_player = self.video.as_ref().map_or_else(
|
||||
|| Element::from(Space::new()),
|
||||
|video| {
|
||||
Element::from(
|
||||
VideoPlayer::new(video)
|
||||
.on_new_frame(Message::NewFrame),
|
||||
)
|
||||
Element::from(VideoPlayer::new(video).on_new_frame(Message::NewFrame))
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -183,16 +169,15 @@ impl VideoEditor {
|
|||
}
|
||||
|
||||
fn toolbar(&self) -> Element<Message> {
|
||||
let title_box = text_input("Title...", &self.title)
|
||||
.on_input(Message::ChangeTitle);
|
||||
let title_box =
|
||||
text_input("Title...", &self.title).on_input(Message::ChangeTitle);
|
||||
|
||||
let video_selector = button::icon(
|
||||
icon::from_name("folder-videos-symbolic").scale(2),
|
||||
)
|
||||
.label("Video")
|
||||
.tooltip("Select a video")
|
||||
.on_press(Message::PickVideo)
|
||||
.padding(10);
|
||||
let video_selector =
|
||||
button::icon(icon::from_name("folder-videos-symbolic").scale(2))
|
||||
.label("Video")
|
||||
.tooltip("Select a video")
|
||||
.on_press(Message::PickVideo)
|
||||
.padding(10);
|
||||
|
||||
row![
|
||||
text::body("Title:"),
|
||||
|
|
@ -217,17 +202,19 @@ impl VideoEditor {
|
|||
self.core_video = Some(video.clone());
|
||||
return;
|
||||
};
|
||||
let Ok(mut player_video) = gst_video::create_video(&url, 60)
|
||||
else {
|
||||
|
||||
let settings = gst_video::VideoSettings {
|
||||
mute: false,
|
||||
framerate: 60,
|
||||
};
|
||||
|
||||
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.title = format!("{}: {}", String::from("Video Missing"), &video.title);
|
||||
self.core_video = Some(video.clone());
|
||||
return;
|
||||
};
|
||||
|
||||
player_video.set_paused(true);
|
||||
self.video = Some(player_video);
|
||||
self.title.clone_from(&video.title);
|
||||
|
|
@ -256,9 +243,7 @@ async fn pick_video() -> Result<PathBuf, VideoError> {
|
|||
error!(?e);
|
||||
VideoError::DialogClosed
|
||||
})
|
||||
.map(|file| {
|
||||
file.url().to_file_path().expect("Should be a file here")
|
||||
})
|
||||
.map(|file| file.url().to_file_path().expect("Should be a file here"))
|
||||
// rfd::AsyncFileDialog::new()
|
||||
// .set_title("Choose a background...")
|
||||
// .add_filter(
|
||||
|
|
|
|||
|
|
@ -29,16 +29,14 @@ use cosmic::iced::advanced::{Clipboard, Shell, overlay, renderer};
|
|||
use cosmic::iced::alignment::{self, Alignment};
|
||||
use cosmic::iced::event::Event;
|
||||
use cosmic::iced::{
|
||||
self, Background, Border, Color, Element, Length, Padding,
|
||||
Pixels, Point, Rectangle, Size, Transformation, Vector, mouse,
|
||||
self, Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle,
|
||||
Size, Transformation, Vector, mouse,
|
||||
};
|
||||
|
||||
use super::{Action, DragEvent, DropPosition};
|
||||
|
||||
pub fn column<'a, Message, Theme, Renderer>(
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Column<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
|
|
@ -72,12 +70,8 @@ const DRAG_DEADBAND_DISTANCE: f32 = 5.0;
|
|||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Column<
|
||||
'a,
|
||||
Message,
|
||||
Theme = cosmic::Theme,
|
||||
Renderer = iced::Renderer,
|
||||
> where
|
||||
pub struct Column<'a, Message, Theme = cosmic::Theme, Renderer = iced::Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
spacing: f32,
|
||||
|
|
@ -93,8 +87,7 @@ pub struct Column<
|
|||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
Column<'a, Message, Theme, Renderer>
|
||||
impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
Theme: Catalog,
|
||||
|
|
@ -113,9 +106,7 @@ where
|
|||
|
||||
/// Creates a [`Column`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
let iterator = children.into_iter();
|
||||
|
||||
|
|
@ -130,9 +121,7 @@ where
|
|||
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||
/// call [`Column::width`] or [`Column::height`] accordingly.
|
||||
#[must_use]
|
||||
pub fn from_vec(
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
pub fn from_vec(children: Vec<Element<'a, Message, Theme, Renderer>>) -> Self {
|
||||
Self {
|
||||
spacing: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
|
|
@ -183,10 +172,7 @@ where
|
|||
}
|
||||
|
||||
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
||||
pub fn align_x(
|
||||
mut self,
|
||||
align: impl Into<alignment::Horizontal>,
|
||||
) -> Self {
|
||||
pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self {
|
||||
self.align = Alignment::from(align.into());
|
||||
self
|
||||
}
|
||||
|
|
@ -222,9 +208,7 @@ where
|
|||
/// Adds an element to the [`Column`], if `Some`.
|
||||
pub fn push_maybe(
|
||||
self,
|
||||
child: Option<
|
||||
impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
>,
|
||||
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||
) -> Self {
|
||||
if let Some(child) = child {
|
||||
self.push(child)
|
||||
|
|
@ -235,10 +219,7 @@ where
|
|||
|
||||
/// Sets the style of the [`Column`].
|
||||
#[must_use]
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Fn(&Theme) -> Style + 'a,
|
||||
) -> Self
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
|
|
@ -248,10 +229,7 @@ where
|
|||
|
||||
/// Sets the style class of the [`Column`].
|
||||
#[must_use]
|
||||
pub fn class(
|
||||
mut self,
|
||||
class: impl Into<Theme::Class<'a>>,
|
||||
) -> Self {
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
|
|
@ -259,18 +237,13 @@ where
|
|||
/// Extends the [`Column`] with the given children.
|
||||
pub fn extend(
|
||||
self,
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
children.into_iter().fold(self, Self::push)
|
||||
}
|
||||
|
||||
/// The message produced by the [`Column`] when a child is dragged.
|
||||
pub fn on_drag(
|
||||
mut self,
|
||||
on_reorder: impl Fn(DragEvent) -> Message + 'a,
|
||||
) -> Self {
|
||||
pub fn on_drag(mut self, on_reorder: impl Fn(DragEvent) -> Message + 'a) -> Self {
|
||||
self.on_drag = Some(Box::new(on_reorder));
|
||||
self
|
||||
}
|
||||
|
|
@ -319,8 +292,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<Message, Renderer> Default
|
||||
for Column<'_, Message, Theme, Renderer>
|
||||
impl<Message, Renderer> Default for Column<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
Theme: Catalog,
|
||||
|
|
@ -336,9 +308,7 @@ impl<'a, Message, Theme, Renderer: renderer::Renderer>
|
|||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn from_iter<
|
||||
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
>(
|
||||
fn from_iter<T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
Self::with_children(iter)
|
||||
|
|
@ -413,9 +383,7 @@ where
|
|||
.for_each(|((child, state), c_layout)| {
|
||||
child.as_widget_mut().operate(
|
||||
state,
|
||||
c_layout.with_virtual_offset(
|
||||
layout.virtual_offset(),
|
||||
),
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
|
|
@ -445,8 +413,7 @@ where
|
|||
child.as_widget_mut().update(
|
||||
state,
|
||||
&event.clone(),
|
||||
c_layout
|
||||
.with_virtual_offset(layout.virtual_offset()),
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
|
|
@ -456,19 +423,10 @@ where
|
|||
});
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(
|
||||
mouse::Button::Left,
|
||||
)) => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position_over(layout.bounds())
|
||||
{
|
||||
for (index, child_layout) in
|
||||
layout.children().enumerate()
|
||||
{
|
||||
if child_layout
|
||||
.bounds()
|
||||
.contains(cursor_position)
|
||||
{
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
|
||||
for (index, child_layout) in layout.children().enumerate() {
|
||||
if child_layout.bounds().contains(cursor_position) {
|
||||
*action = Action::Picking {
|
||||
index,
|
||||
origin: cursor_position,
|
||||
|
|
@ -482,10 +440,8 @@ where
|
|||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||
match *action {
|
||||
Action::Picking { index, origin } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
&& cursor_position.distance(origin)
|
||||
> self.deadband_zone
|
||||
if let Some(cursor_position) = cursor.position()
|
||||
&& cursor_position.distance(origin) > self.deadband_zone
|
||||
{
|
||||
// Start dragging
|
||||
*action = Action::Dragging {
|
||||
|
|
@ -494,17 +450,13 @@ where
|
|||
last_cursor: cursor_position,
|
||||
};
|
||||
if let Some(on_reorder) = &self.on_drag {
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Picked { index },
|
||||
));
|
||||
shell.publish(on_reorder(DragEvent::Picked { index }));
|
||||
}
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Action::Dragging { origin, index, .. } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
{
|
||||
if let Some(cursor_position) = cursor.position() {
|
||||
*action = Action::Dragging {
|
||||
last_cursor: cursor_position,
|
||||
origin,
|
||||
|
|
@ -516,41 +468,25 @@ where
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
)) => {
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||
match *action {
|
||||
Action::Dragging { index, .. } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
{
|
||||
if let Some(cursor_position) = cursor.position() {
|
||||
let bounds = layout.bounds();
|
||||
if bounds.contains(cursor_position) {
|
||||
let (target_index, drop_position) =
|
||||
self.compute_target_index(
|
||||
cursor_position,
|
||||
layout,
|
||||
index,
|
||||
);
|
||||
let (target_index, drop_position) = self
|
||||
.compute_target_index(cursor_position, layout, index);
|
||||
|
||||
if let Some(on_reorder) =
|
||||
&self.on_drag
|
||||
{
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Dropped {
|
||||
index,
|
||||
target_index,
|
||||
drop_position,
|
||||
},
|
||||
));
|
||||
if let Some(on_reorder) = &self.on_drag {
|
||||
shell.publish(on_reorder(DragEvent::Dropped {
|
||||
index,
|
||||
target_index,
|
||||
drop_position,
|
||||
}));
|
||||
shell.capture_event();
|
||||
}
|
||||
} else if let Some(on_reorder) =
|
||||
&self.on_drag
|
||||
{
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Canceled { index },
|
||||
));
|
||||
} else if let Some(on_reorder) = &self.on_drag {
|
||||
shell.publish(on_reorder(DragEvent::Canceled { index }));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
|
|
@ -588,8 +524,7 @@ where
|
|||
.map(|((child, state), c_layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
c_layout
|
||||
.with_virtual_offset(layout.virtual_offset()),
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
|
|
@ -623,20 +558,15 @@ where
|
|||
|
||||
// Determine the target index based on cursor position
|
||||
let target_index = if cursor.position().is_some() {
|
||||
let (target_index, _) = self
|
||||
.compute_target_index(
|
||||
*last_cursor,
|
||||
layout,
|
||||
*index,
|
||||
);
|
||||
let (target_index, _) =
|
||||
self.compute_target_index(*last_cursor, layout, *index);
|
||||
target_index.min(child_count - 1)
|
||||
} else {
|
||||
*index
|
||||
};
|
||||
|
||||
// Store the width of the dragged item
|
||||
let drag_bounds =
|
||||
layout.children().nth(*index).unwrap().bounds();
|
||||
let drag_bounds = layout.children().nth(*index).unwrap().bounds();
|
||||
let drag_height = drag_bounds.height + self.spacing;
|
||||
|
||||
// Draw all children except the one being dragged
|
||||
|
|
@ -644,125 +574,92 @@ where
|
|||
for i in 0..child_count {
|
||||
let child = &self.children[i];
|
||||
let state = &tree.children[i];
|
||||
let child_layout =
|
||||
layout.children().nth(i).unwrap();
|
||||
let child_layout = layout.children().nth(i).unwrap();
|
||||
|
||||
// Draw the dragged item separately
|
||||
// TODO: Draw a shadow below the picked item to enhance the
|
||||
// floating effect
|
||||
if i == *index {
|
||||
let scaling =
|
||||
Transformation::scale(style.scale);
|
||||
let translation =
|
||||
*last_cursor - *origin * scaling;
|
||||
renderer.with_translation(
|
||||
translation,
|
||||
|renderer| {
|
||||
renderer.with_transformation(
|
||||
scaling,
|
||||
|renderer| {
|
||||
renderer.with_layer(
|
||||
child_layout.bounds(),
|
||||
|renderer| {
|
||||
child
|
||||
.as_widget()
|
||||
.draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
default_style,
|
||||
child_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let offset: i32 =
|
||||
match target_index.cmp(index) {
|
||||
std::cmp::Ordering::Less
|
||||
if i >= target_index
|
||||
&& i < *index =>
|
||||
{
|
||||
1
|
||||
}
|
||||
std::cmp::Ordering::Greater
|
||||
if i > *index
|
||||
&& i <= target_index =>
|
||||
{
|
||||
-1
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let translation = Vector::new(
|
||||
0.0,
|
||||
offset as f32 * drag_height,
|
||||
);
|
||||
renderer.with_translation(
|
||||
translation,
|
||||
|renderer| {
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
default_style,
|
||||
child_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
// Draw an overlay if this item is being moved
|
||||
// TODO: instead of drawing an overlay, it would be nicer to
|
||||
// draw the item with a reduced opacity, but that's not possible today
|
||||
if offset != 0 {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: child_layout
|
||||
.bounds(),
|
||||
..renderer::Quad::default(
|
||||
)
|
||||
},
|
||||
style.moved_item_overlay,
|
||||
let scaling = Transformation::scale(style.scale);
|
||||
let translation = *last_cursor - *origin * scaling;
|
||||
renderer.with_translation(translation, |renderer| {
|
||||
renderer.with_transformation(scaling, |renderer| {
|
||||
renderer.with_layer(child_layout.bounds(), |renderer| {
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
default_style,
|
||||
child_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let offset: i32 = match target_index.cmp(index) {
|
||||
std::cmp::Ordering::Less
|
||||
if i >= target_index && i < *index =>
|
||||
{
|
||||
1
|
||||
}
|
||||
std::cmp::Ordering::Greater
|
||||
if i > *index && i <= target_index =>
|
||||
{
|
||||
-1
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
);
|
||||
let translation = Vector::new(0.0, offset as f32 * drag_height);
|
||||
renderer.with_translation(translation, |renderer| {
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
default_style,
|
||||
child_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
// Draw an overlay if this item is being moved
|
||||
// TODO: instead of drawing an overlay, it would be nicer to
|
||||
// draw the item with a reduced opacity, but that's not possible today
|
||||
if offset != 0 {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: child_layout.bounds(),
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.moved_item_overlay,
|
||||
);
|
||||
|
||||
// Keep track of the total translation so we can
|
||||
// draw the "ghost" of the dragged item later
|
||||
translations -= (child_layout.bounds().height
|
||||
+ self.spacing)
|
||||
* offset.signum() as f32;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Draw a ghost of the dragged item in its would-be position
|
||||
let ghost_translation =
|
||||
Vector::new(0.0, translations);
|
||||
renderer.with_translation(
|
||||
ghost_translation,
|
||||
|renderer| {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: drag_bounds,
|
||||
border: style.ghost_border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.ghost_background,
|
||||
);
|
||||
},
|
||||
);
|
||||
let ghost_translation = Vector::new(0.0, translations);
|
||||
renderer.with_translation(ghost_translation, |renderer| {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: drag_bounds,
|
||||
border: style.ghost_border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.ghost_background,
|
||||
);
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Draw all children normally when not dragging
|
||||
if let Some(clipped_viewport) =
|
||||
layout.bounds().intersection(viewport)
|
||||
{
|
||||
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) {
|
||||
let viewport = if self.clip {
|
||||
&clipped_viewport
|
||||
} else {
|
||||
|
|
@ -773,18 +670,14 @@ where
|
|||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.filter(|(_, layout)| {
|
||||
layout.bounds().intersects(viewport)
|
||||
})
|
||||
.filter(|(_, layout)| layout.bounds().intersects(viewport))
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
default_style,
|
||||
c_layout.with_virtual_offset(
|
||||
layout.virtual_offset(),
|
||||
),
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
|
|
@ -835,8 +728,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
From<Column<'a, Message, Theme, Renderer>>
|
||||
impl<'a, Message, Theme, Renderer> From<Column<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
|
|
@ -892,8 +784,7 @@ impl Catalog for cosmic::Theme {
|
|||
pub fn default(theme: &Theme) -> Style {
|
||||
Style {
|
||||
scale: 1.05,
|
||||
moved_item_overlay: Color::from(theme.cosmic().primary.base)
|
||||
.scale_alpha(0.2),
|
||||
moved_item_overlay: Color::from(theme.cosmic().primary.base).scale_alpha(0.2),
|
||||
ghost_border: Border {
|
||||
width: 1.0,
|
||||
color: theme.cosmic().secondary.base.into(),
|
||||
|
|
|
|||
|
|
@ -29,16 +29,14 @@ use cosmic::iced::advanced::{Clipboard, Shell, overlay, renderer};
|
|||
use cosmic::iced::alignment::{self, Alignment};
|
||||
use cosmic::iced::event::Event;
|
||||
use cosmic::iced::{
|
||||
self, Background, Border, Color, Element, Length, Padding,
|
||||
Pixels, Point, Rectangle, Size, Transformation, Vector, mouse,
|
||||
self, Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle,
|
||||
Size, Transformation, Vector, mouse,
|
||||
};
|
||||
|
||||
use super::{Action, DragEvent, DropPosition};
|
||||
|
||||
pub fn row<'a, Message, Theme, Renderer>(
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Row<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
|
|
@ -107,9 +105,7 @@ where
|
|||
|
||||
/// Creates a [`Row`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
let iterator = children.into_iter();
|
||||
|
||||
|
|
@ -124,9 +120,7 @@ where
|
|||
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||
/// call [`Row::width`] or [`Row::height`] accordingly.
|
||||
#[must_use]
|
||||
pub fn from_vec(
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
pub fn from_vec(children: Vec<Element<'a, Message, Theme, Renderer>>) -> Self {
|
||||
Self {
|
||||
spacing: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
|
|
@ -170,10 +164,7 @@ where
|
|||
}
|
||||
|
||||
/// Sets the vertical alignment of the contents of the [`Row`] .
|
||||
pub fn align_y(
|
||||
mut self,
|
||||
align: impl Into<alignment::Vertical>,
|
||||
) -> Self {
|
||||
pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self {
|
||||
self.align = Alignment::from(align.into());
|
||||
self
|
||||
}
|
||||
|
|
@ -209,9 +200,7 @@ where
|
|||
/// Adds an element to the [`Row`], if `Some`.
|
||||
pub fn push_maybe(
|
||||
self,
|
||||
child: Option<
|
||||
impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
>,
|
||||
child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
|
||||
) -> Self {
|
||||
if let Some(child) = child {
|
||||
self.push(child)
|
||||
|
|
@ -222,10 +211,7 @@ where
|
|||
|
||||
/// Sets the style of the [`Row`].
|
||||
#[must_use]
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Fn(&Theme) -> Style + 'a,
|
||||
) -> Self
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
|
|
@ -235,10 +221,7 @@ where
|
|||
|
||||
/// Sets the style class of the [`Row`].
|
||||
#[must_use]
|
||||
pub fn class(
|
||||
mut self,
|
||||
class: impl Into<Theme::Class<'a>>,
|
||||
) -> Self {
|
||||
pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
|
|
@ -246,9 +229,7 @@ where
|
|||
/// Extends the [`Row`] with the given children.
|
||||
pub fn extend(
|
||||
self,
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
children.into_iter().fold(self, Self::push)
|
||||
}
|
||||
|
|
@ -256,17 +237,12 @@ where
|
|||
/// Turns the [`Row`] into a [`Wrapping`] row.
|
||||
///
|
||||
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
||||
pub const fn wrap(
|
||||
self,
|
||||
) -> Wrapping<'a, Message, Theme, Renderer> {
|
||||
pub const fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> {
|
||||
Wrapping { row: self }
|
||||
}
|
||||
|
||||
/// The message produced by the [`Row`] when a child is dragged.
|
||||
pub fn on_drag(
|
||||
mut self,
|
||||
on_reorder: impl Fn(DragEvent) -> Message + 'a,
|
||||
) -> Self {
|
||||
pub fn on_drag(mut self, on_reorder: impl Fn(DragEvent) -> Message + 'a) -> Self {
|
||||
self.on_drag = Some(Box::new(on_reorder));
|
||||
self
|
||||
}
|
||||
|
|
@ -331,9 +307,7 @@ impl<'a, Message, Theme, Renderer: renderer::Renderer>
|
|||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn from_iter<
|
||||
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
>(
|
||||
fn from_iter<T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
Self::with_children(iter)
|
||||
|
|
@ -442,19 +416,10 @@ where
|
|||
});
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(
|
||||
mouse::Button::Left,
|
||||
)) => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position_over(layout.bounds())
|
||||
{
|
||||
for (index, child_layout) in
|
||||
layout.children().enumerate()
|
||||
{
|
||||
if child_layout
|
||||
.bounds()
|
||||
.contains(cursor_position)
|
||||
{
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
|
||||
for (index, child_layout) in layout.children().enumerate() {
|
||||
if child_layout.bounds().contains(cursor_position) {
|
||||
*action = Action::Picking {
|
||||
index,
|
||||
origin: cursor_position,
|
||||
|
|
@ -468,10 +433,8 @@ where
|
|||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||
match *action {
|
||||
Action::Picking { index, origin } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
&& cursor_position.distance(origin)
|
||||
> self.deadband_zone
|
||||
if let Some(cursor_position) = cursor.position()
|
||||
&& cursor_position.distance(origin) > self.deadband_zone
|
||||
{
|
||||
// Start dragging
|
||||
*action = Action::Dragging {
|
||||
|
|
@ -480,17 +443,13 @@ where
|
|||
last_cursor: cursor_position,
|
||||
};
|
||||
if let Some(on_reorder) = &self.on_drag {
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Picked { index },
|
||||
));
|
||||
shell.publish(on_reorder(DragEvent::Picked { index }));
|
||||
}
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Action::Dragging { origin, index, .. } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
{
|
||||
if let Some(cursor_position) = cursor.position() {
|
||||
*action = Action::Dragging {
|
||||
last_cursor: cursor_position,
|
||||
origin,
|
||||
|
|
@ -502,41 +461,25 @@ where
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
)) => {
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
|
||||
match *action {
|
||||
Action::Dragging { index, .. } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
{
|
||||
if let Some(cursor_position) = cursor.position() {
|
||||
let bounds = layout.bounds();
|
||||
if bounds.contains(cursor_position) {
|
||||
let (target_index, drop_position) =
|
||||
self.compute_target_index(
|
||||
cursor_position,
|
||||
layout,
|
||||
index,
|
||||
);
|
||||
let (target_index, drop_position) = self
|
||||
.compute_target_index(cursor_position, layout, index);
|
||||
|
||||
if let Some(on_reorder) =
|
||||
&self.on_drag
|
||||
{
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Dropped {
|
||||
index,
|
||||
target_index,
|
||||
drop_position,
|
||||
},
|
||||
));
|
||||
if let Some(on_reorder) = &self.on_drag {
|
||||
shell.publish(on_reorder(DragEvent::Dropped {
|
||||
index,
|
||||
target_index,
|
||||
drop_position,
|
||||
}));
|
||||
shell.capture_event();
|
||||
}
|
||||
} else if let Some(on_reorder) =
|
||||
&self.on_drag
|
||||
{
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Canceled { index },
|
||||
));
|
||||
} else if let Some(on_reorder) = &self.on_drag {
|
||||
shell.publish(on_reorder(DragEvent::Canceled { index }));
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
|
|
@ -572,9 +515,9 @@ where
|
|||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state, layout, cursor, viewport, renderer,
|
||||
)
|
||||
child
|
||||
.as_widget()
|
||||
.mouse_interaction(state, layout, cursor, viewport, renderer)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
|
|
@ -604,20 +547,15 @@ where
|
|||
|
||||
// Determine the target index based on cursor position
|
||||
let target_index = if cursor.position().is_some() {
|
||||
let (target_index, _) = self
|
||||
.compute_target_index(
|
||||
*last_cursor,
|
||||
layout,
|
||||
*index,
|
||||
);
|
||||
let (target_index, _) =
|
||||
self.compute_target_index(*last_cursor, layout, *index);
|
||||
target_index.min(child_count - 1)
|
||||
} else {
|
||||
*index
|
||||
};
|
||||
|
||||
// Store the width of the dragged item
|
||||
let drag_bounds =
|
||||
layout.children().nth(*index).unwrap().bounds();
|
||||
let drag_bounds = layout.children().nth(*index).unwrap().bounds();
|
||||
let drag_width = drag_bounds.width + self.spacing;
|
||||
|
||||
// Draw all children except the one being dragged
|
||||
|
|
@ -625,118 +563,88 @@ where
|
|||
for i in 0..child_count {
|
||||
let child = &self.children[i];
|
||||
let state = &tree.children[i];
|
||||
let child_layout =
|
||||
layout.children().nth(i).unwrap();
|
||||
let child_layout = layout.children().nth(i).unwrap();
|
||||
|
||||
// Draw the dragged item separately
|
||||
// TODO: Draw a shadow below the picked item to enhance the
|
||||
// floating effect
|
||||
if i == *index {
|
||||
let scaling =
|
||||
Transformation::scale(style.scale);
|
||||
let translation =
|
||||
*last_cursor - *origin * scaling;
|
||||
renderer.with_translation(
|
||||
translation,
|
||||
|renderer| {
|
||||
renderer.with_transformation(
|
||||
scaling,
|
||||
|renderer| {
|
||||
renderer.with_layer(
|
||||
child_layout.bounds(),
|
||||
|renderer| {
|
||||
child
|
||||
.as_widget()
|
||||
.draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
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,
|
||||
let scaling = Transformation::scale(style.scale);
|
||||
let translation = *last_cursor - *origin * scaling;
|
||||
renderer.with_translation(translation, |renderer| {
|
||||
renderer.with_transformation(scaling, |renderer| {
|
||||
renderer.with_layer(child_layout.bounds(), |renderer| {
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
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,
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
);
|
||||
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,
|
||||
);
|
||||
|
||||
// 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
|
||||
let ghost_translation =
|
||||
Vector::new(translations, 0.0);
|
||||
renderer.with_translation(
|
||||
ghost_translation,
|
||||
|renderer| {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: drag_bounds,
|
||||
border: style.ghost_border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.ghost_background,
|
||||
);
|
||||
},
|
||||
);
|
||||
let ghost_translation = Vector::new(translations, 0.0);
|
||||
renderer.with_translation(ghost_translation, |renderer| {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: drag_bounds,
|
||||
border: style.ghost_border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.ghost_background,
|
||||
);
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// Draw all children normally when not dragging
|
||||
|
|
@ -746,10 +654,9 @@ where
|
|||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state, renderer, theme, defaults, layout,
|
||||
cursor, viewport,
|
||||
);
|
||||
child
|
||||
.as_widget()
|
||||
.draw(state, renderer, theme, defaults, layout, cursor, viewport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -774,8 +681,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
From<Row<'a, Message, Theme, Renderer>>
|
||||
impl<'a, Message, Theme, Renderer> From<Row<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
|
|
@ -794,12 +700,8 @@ where
|
|||
///
|
||||
/// The original alignment of the [`Row`] is preserved per row wrapped.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Wrapping<
|
||||
'a,
|
||||
Message,
|
||||
Theme = cosmic::Theme,
|
||||
Renderer = iced::Renderer,
|
||||
> where
|
||||
pub struct Wrapping<'a, Message, Theme = cosmic::Theme, Renderer = iced::Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
row: Row<'a, Message, Theme, Renderer>,
|
||||
|
|
@ -850,34 +752,31 @@ where
|
|||
Alignment::End => 1.0,
|
||||
};
|
||||
|
||||
let align =
|
||||
|row_start: std::ops::Range<usize>,
|
||||
row_height: f32,
|
||||
children: &mut Vec<layout::Node>| {
|
||||
if align_factor != 0.0 {
|
||||
for node in &mut children[row_start] {
|
||||
let height = node.size().height;
|
||||
let align = |row_start: std::ops::Range<usize>,
|
||||
row_height: f32,
|
||||
children: &mut Vec<layout::Node>| {
|
||||
if align_factor != 0.0 {
|
||||
for node in &mut children[row_start] {
|
||||
let height = node.size().height;
|
||||
|
||||
node.translate_mut(Vector::new(
|
||||
0.0,
|
||||
(row_height - height) / align_factor,
|
||||
));
|
||||
}
|
||||
node.translate_mut(Vector::new(
|
||||
0.0,
|
||||
(row_height - height) / align_factor,
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
for (i, child) in self.row.children.iter_mut().enumerate() {
|
||||
let node = child.as_widget_mut().layout(
|
||||
&mut tree.children[i],
|
||||
renderer,
|
||||
&limits,
|
||||
);
|
||||
let node =
|
||||
child
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[i], renderer, &limits);
|
||||
|
||||
let child_size = node.size();
|
||||
|
||||
if x != 0.0 && x + child_size.width > max_width {
|
||||
intrinsic_size.width =
|
||||
intrinsic_size.width.max(x - spacing);
|
||||
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
|
||||
|
||||
align(row_start..i, row_height, &mut children);
|
||||
|
||||
|
|
@ -889,32 +788,23 @@ where
|
|||
|
||||
row_height = row_height.max(child_size.height);
|
||||
|
||||
children.push(node.move_to((
|
||||
x + self.row.padding.left,
|
||||
y + self.row.padding.top,
|
||||
)));
|
||||
children.push(
|
||||
node.move_to((x + self.row.padding.left, y + self.row.padding.top)),
|
||||
);
|
||||
|
||||
x += child_size.width + spacing;
|
||||
}
|
||||
|
||||
if x != 0.0 {
|
||||
intrinsic_size.width =
|
||||
intrinsic_size.width.max(x - spacing);
|
||||
intrinsic_size.width = intrinsic_size.width.max(x - spacing);
|
||||
}
|
||||
|
||||
intrinsic_size.height = y + row_height;
|
||||
align(row_start..children.len(), row_height, &mut children);
|
||||
|
||||
let size = limits.resolve(
|
||||
self.row.width,
|
||||
self.row.height,
|
||||
intrinsic_size,
|
||||
);
|
||||
let size = limits.resolve(self.row.width, self.row.height, intrinsic_size);
|
||||
|
||||
layout::Node::with_children(
|
||||
size.expand(self.row.padding),
|
||||
children,
|
||||
)
|
||||
layout::Node::with_children(size.expand(self.row.padding), children)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
|
|
@ -939,8 +829,7 @@ where
|
|||
viewport: &Rectangle,
|
||||
) {
|
||||
self.row.update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell,
|
||||
viewport,
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -952,9 +841,8 @@ where
|
|||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.row.mouse_interaction(
|
||||
tree, layout, cursor, viewport, renderer,
|
||||
)
|
||||
self.row
|
||||
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -967,9 +855,8 @@ where
|
|||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.row.draw(
|
||||
tree, renderer, theme, style, layout, cursor, viewport,
|
||||
);
|
||||
self.row
|
||||
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
|
|
@ -980,18 +867,12 @@ where
|
|||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
self.row.overlay(
|
||||
tree,
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
self.row
|
||||
.overlay(tree, layout, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
From<Wrapping<'a, Message, Theme, Renderer>>
|
||||
impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
|
|
@ -1047,19 +928,15 @@ impl Catalog for cosmic::Theme {
|
|||
pub fn default(theme: &cosmic::Theme) -> Style {
|
||||
Style {
|
||||
scale: 1.05,
|
||||
moved_item_overlay: Color::from(
|
||||
theme.cosmic().primary.base.color,
|
||||
)
|
||||
.scale_alpha(0.2),
|
||||
moved_item_overlay: Color::from(theme.cosmic().primary.base.color)
|
||||
.scale_alpha(0.2),
|
||||
ghost_border: Border {
|
||||
width: 1.0,
|
||||
color: theme.cosmic().secondary.base.color.into(),
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
ghost_background: Color::from(
|
||||
theme.cosmic().secondary.base.color,
|
||||
)
|
||||
.scale_alpha(0.2)
|
||||
.into(),
|
||||
ghost_background: Color::from(theme.cosmic().secondary.base.color)
|
||||
.scale_alpha(0.2)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ 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,
|
||||
Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse,
|
||||
overlay, renderer,
|
||||
};
|
||||
|
||||
pub fn loaded_image<'a, Message: 'static, Theme, Renderer>(
|
||||
|
|
@ -15,9 +15,7 @@ where
|
|||
<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,
|
||||
>,
|
||||
+ iced_core::image::Renderer<Handle = cosmic::widget::image::Handle>,
|
||||
<Renderer as iced_core::image::Renderer>::Handle: 'a,
|
||||
{
|
||||
LoadedImage::new(handle, content)
|
||||
|
|
@ -37,8 +35,7 @@ where
|
|||
content: cosmic::iced::Element<'a, Message, Theme, Renderer>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
LoadedImage<'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,
|
||||
|
|
@ -46,9 +43,7 @@ where
|
|||
/// 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>,
|
||||
>,
|
||||
content: impl Into<cosmic::iced::Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
LoadedImage {
|
||||
handle,
|
||||
|
|
@ -80,11 +75,10 @@ where
|
|||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let node = self.content.as_widget_mut().layout(
|
||||
&mut tree.children[0],
|
||||
renderer,
|
||||
limits,
|
||||
);
|
||||
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])
|
||||
}
|
||||
|
|
@ -153,8 +147,7 @@ where
|
|||
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
content_layout
|
||||
.with_virtual_offset(layout.virtual_offset()),
|
||||
content_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
|
|
@ -183,8 +176,7 @@ where
|
|||
renderer,
|
||||
theme,
|
||||
renderer_style,
|
||||
content_layout
|
||||
.with_virtual_offset(layout.virtual_offset()),
|
||||
content_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
|
|
@ -224,8 +216,7 @@ where
|
|||
.expect("There should always be a child");
|
||||
self.content.as_widget().drag_destinations(
|
||||
&state.children[0],
|
||||
content_layout
|
||||
.with_virtual_offset(layout.virtual_offset()),
|
||||
content_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
renderer,
|
||||
dnd_rectangles,
|
||||
);
|
||||
|
|
@ -233,8 +224,7 @@ where
|
|||
}
|
||||
|
||||
#[allow(clippy::use_self)]
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
From<LoadedImage<'a, Message, Theme, Renderer>>
|
||||
impl<'a, Message, Theme, Renderer> From<LoadedImage<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
use cosmic::cosmic_theme::palette::WithAlpha;
|
||||
use cosmic::iced::widget::{column, row};
|
||||
use cosmic::iced::{Background, Border};
|
||||
use cosmic::widget::{
|
||||
button, combo_box, container, icon, space, text_editor,
|
||||
};
|
||||
use cosmic::widget::{button, combo_box, container, icon, space, text_editor};
|
||||
use cosmic::{Element, Task, theme};
|
||||
|
||||
use crate::core::songs::VerseName;
|
||||
|
|
@ -44,9 +42,7 @@ impl VerseEditor {
|
|||
lyric: lyric.to_string(),
|
||||
content: text_editor::Content::with_text(lyric),
|
||||
editing_verse_name: false,
|
||||
verse_name_combo: combo_box::State::new(
|
||||
VerseName::all_names(),
|
||||
),
|
||||
verse_name_combo: combo_box::State::new(VerseName::all_names()),
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self, message: Message) -> Action {
|
||||
|
|
@ -72,9 +68,7 @@ impl VerseEditor {
|
|||
Action::None
|
||||
}
|
||||
},
|
||||
Message::UpdateVerseName(verse_name) => {
|
||||
Action::UpdateVerseName(verse_name)
|
||||
}
|
||||
Message::UpdateVerseName(verse_name) => Action::UpdateVerseName(verse_name),
|
||||
Message::EditVerseName => {
|
||||
self.editing_verse_name = !self.editing_verse_name;
|
||||
Action::None
|
||||
|
|
@ -93,9 +87,7 @@ impl VerseEditor {
|
|||
} = theme::spacing();
|
||||
|
||||
let delete_button = button::text("Delete")
|
||||
.trailing_icon(
|
||||
icon::from_name("view-close").symbolic(true),
|
||||
)
|
||||
.trailing_icon(icon::from_name("view-close").symbolic(true))
|
||||
.class(theme::Button::Destructive)
|
||||
.on_press(Message::DeleteVerse(self.verse_name));
|
||||
let combo = combo_box(
|
||||
|
|
@ -105,59 +97,42 @@ impl VerseEditor {
|
|||
Message::UpdateVerseName,
|
||||
);
|
||||
|
||||
let verse_title =
|
||||
row![combo, space::horizontal(), delete_button];
|
||||
let verse_title = row![combo, space::horizontal(), delete_button];
|
||||
|
||||
let lyric: Element<Message> = if self.verse_name
|
||||
== VerseName::Blank
|
||||
{
|
||||
let lyric: Element<Message> = if self.verse_name == VerseName::Blank {
|
||||
space::horizontal().into()
|
||||
} else {
|
||||
text_editor(&self.content)
|
||||
.on_action(Message::UpdateLyric)
|
||||
.padding(space_m)
|
||||
.class(theme::iced::TextEditor::Custom(Box::new(
|
||||
move |t, s| {
|
||||
let neutral = t.cosmic().palette.neutral_9;
|
||||
let mut base_style = text_editor::Style {
|
||||
background: Background::Color(
|
||||
t.cosmic()
|
||||
.background
|
||||
.small_widget
|
||||
.with_alpha(0.25)
|
||||
.into(),
|
||||
),
|
||||
border: Border::default()
|
||||
.rounded(space_s as u8)
|
||||
.width(2)
|
||||
.color(
|
||||
t.cosmic().bg_component_divider(),
|
||||
),
|
||||
placeholder: neutral
|
||||
.with_alpha(0.7)
|
||||
.into(),
|
||||
value: neutral.into(),
|
||||
selection: t.cosmic().accent.base.into(),
|
||||
};
|
||||
let hovered_border = Border::default()
|
||||
.class(theme::iced::TextEditor::Custom(Box::new(move |t, s| {
|
||||
let neutral = t.cosmic().palette.neutral_9;
|
||||
let mut base_style = text_editor::Style {
|
||||
background: Background::Color(
|
||||
t.cosmic().background.small_widget.with_alpha(0.25).into(),
|
||||
),
|
||||
border: Border::default()
|
||||
.rounded(space_s as u8)
|
||||
.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
|
||||
}
|
||||
.width(2)
|
||||
.color(t.cosmic().bg_component_divider()),
|
||||
placeholder: neutral.with_alpha(0.7).into(),
|
||||
value: neutral.into(),
|
||||
selection: t.cosmic().accent.base.into(),
|
||||
};
|
||||
let hovered_border = Border::default()
|
||||
.rounded(space_s as u8)
|
||||
.width(3)
|
||||
.color(t.cosmic().accent.hover);
|
||||
match s {
|
||||
text_editor::Status::Active => base_style,
|
||||
text_editor::Status::Hovered
|
||||
| text_editor::Status::Focused { .. } => {
|
||||
base_style.border = hovered_border;
|
||||
base_style
|
||||
}
|
||||
},
|
||||
)))
|
||||
text_editor::Status::Disabled => base_style,
|
||||
}
|
||||
})))
|
||||
.height(150)
|
||||
.into()
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue