diff --git a/Cargo.lock b/Cargo.lock index 17c5563..315a996 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2071,7 +2071,7 @@ dependencies = [ "log", "rustc-hash 1.1.0", "spirv", - "strum", + "strum 0.26.3", "termcolor", "thiserror 2.0.12", "unicode-xid", @@ -2845,6 +2845,8 @@ dependencies = [ "rust-embed", "serde", "serde_json", + "strum 0.27.1", + "strum_macros 0.27.1", "tempfile", "url", ] @@ -3225,9 +3227,15 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", ] +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" + [[package]] name = "strum_macros" version = "0.26.4" @@ -3241,6 +3249,19 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "svg_fmt" version = "0.4.4" diff --git a/Cargo.toml b/Cargo.toml index 0587959..e67544a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ iced_video_player = {path = "./iced_video_player"} url = "2.5.4" rust-embed = "8.7.0" file-format = "0.26.0" +strum = "0.27.1" +strum_macros = "0.27.1" [profile.release] strip = true # Automatically strip symbols from the binary. diff --git a/iced_video_player/src/video_player.rs b/iced_video_player/src/video_player.rs index f27f6c0..8cccb7d 100644 --- a/iced_video_player/src/video_player.rs +++ b/iced_video_player/src/video_player.rs @@ -10,27 +10,27 @@ use std::{marker::PhantomData, sync::atomic::Ordering}; use std::{sync::Arc, time::Instant}; /// Video player widget which displays the current frame of a [`Video`](crate::Video). -pub struct VideoPlayer +pub struct VideoPlayer<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> where Renderer: PrimitiveRenderer { - video: Video, + video: &'a Video, content_fit: iced::ContentFit, width: iced::Length, height: iced::Length, on_end_of_stream: Option, on_new_frame: Option, - on_subtitle_text: Option) -> Message>>, - on_error: Option Message>>, + on_subtitle_text: Option) -> Message + 'a>>, + on_error: Option Message + 'a>>, _phantom: PhantomData<(Theme, Renderer)>, } -impl VideoPlayer +impl<'a, Message, Theme, Renderer> VideoPlayer<'a, Message, Theme, Renderer> where Renderer: PrimitiveRenderer { /// Creates a new video player widget for a given video. - pub fn new(video: Video) -> Self { + pub fn new(video: &'a Video) -> Self { VideoPlayer { video, content_fit: iced::ContentFit::default(), @@ -108,7 +108,7 @@ where } impl Widget - for VideoPlayer + for VideoPlayer<'_, Message, Theme, Renderer> where Message: Clone, Renderer: PrimitiveRenderer, @@ -292,14 +292,14 @@ where } } -impl From> - for Element<'_, Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> where - Message: 'static + Clone, - Theme: 'static, - Renderer: 'static + PrimitiveRenderer, + Message: 'a + Clone, + Theme: 'a, + Renderer: 'a + PrimitiveRenderer, { - fn from(video_player: VideoPlayer) -> Self { + fn from(video_player: VideoPlayer<'a, Message, Theme, Renderer>) -> Self { Self::new(video_player) } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 86c7606..3c83abe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,19 @@ +mod utils; + use file_format::FileFormat; -use ::image::ImageReader; +use ::image::{DynamicImage, ImageReader}; use iced::{ - alignment::Vertical::Top, border, gradient, mouse, widget::{button, center, column, container, image, mouse_area, row, stack, text, Column, Space}, window::{self, icon, Settings}, Alignment::Center, Color, Element, Font, Length, Point, Renderer, Size, Subscription, Task, Theme + alignment::Vertical::Top, border, gradient, mouse, wgpu::naga::back, widget::{button, center, column, container, image, mouse_area, row, stack, text, Column, Space}, window::{self, icon, Settings}, Alignment::Center, Color, Element, Font, Length, Point, Renderer, Size, Subscription, Task, Theme }; use iced_video_player::{Video, VideoPlayer}; use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; use utils::img_utils::round_image; use std::{ - env, fs::{self, create_dir_all, read_to_string}, io::{Cursor, Read, Write}, path::PathBuf, sync::Arc + collections::HashMap, env, fs::{self, create_dir_all, read_to_string}, io::{Cursor, Read, Write}, path::PathBuf, sync::Arc }; -mod utils; use tempfile::NamedTempFile; -use iced::event::{self, Event}; -use iced::keyboard::Event as KeyboardEvent; -use iced::mouse::Event as MouseEvent; #[derive(rust_embed::Embed)] #[folder = "resources"] @@ -38,7 +38,7 @@ pub fn main() -> iced::Result { max_size: None, min_size: None, visible: true, - resizable: true, + resizable: false, transparent: false, level: window::Level::Normal, exit_on_close_request: true, @@ -52,7 +52,7 @@ pub fn main() -> iced::Result { .run() } -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, EnumIter, Default, Serialize, Deserialize)] enum PossibleGames { #[default] WutheringWaves, @@ -64,31 +64,55 @@ enum PossibleGames { #[derive(Debug)] enum Launcher { Loading, - Loaded(State), + Loaded(Box), } -#[derive(Debug, Default, Clone)] +#[derive(Debug)] +enum LauncherBackground { + Video(Video), + Image(image::Handle), +} + + +impl LauncherBackground { + fn inner(&self) -> Element { + match self { + LauncherBackground::Video(video) => VideoPlayer::new(video).into(), + LauncherBackground::Image(handle) => image(handle).into(), + } + } +} + +#[derive(Debug, Default)] struct State { selected_game: PossibleGames, installed_games: Vec, installed_game_servers: Vec, db_software_installed: bool, + background: Option, + icon_images: HashMap } -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] struct SavedState { installed_games: Vec, installed_game_servers: Vec, db_software_installed: bool, } +impl From for Box { + fn from(val: SavedState) -> Self { + Box::new(State { installed_games: val.installed_games, installed_game_servers: val.installed_game_servers, db_software_installed: val.db_software_installed, ..State::default() }) + } +} + #[derive(Debug, Clone)] enum LoadError { File, Format, } -#[derive(Debug)] +#[derive(Debug, Clone)] enum SaveError { Write, Format, @@ -96,7 +120,7 @@ enum SaveError { #[derive(Debug, Clone)] enum Message { - Loaded(Result), + Loaded(Result), DragStarted, GameSelected(PossibleGames) } @@ -151,8 +175,8 @@ fn rad(deg: f32) -> f32 { deg * std::f32::consts::PI / 180.0 } -fn get_game_background(game: &PossibleGames) -> Element { - let file_path: &str = match game { +fn get_game_background(state: &State) -> LauncherBackground { + let file_path: &str = match state.selected_game { PossibleGames::WutheringWaves => "wutheringwaves-bg.mp4", PossibleGames::ZenlessZoneZero => "zenlesszonezero-bg.png", PossibleGames::HonkaiStarRail => "honkaistarrail-bg.png", @@ -170,8 +194,7 @@ fn get_game_background(game: &PossibleGames) -> Element { match Video::new(url::Url::from_file_path(temp_path).unwrap()) { Ok(mut video) => { video.set_looping(true); - - VideoPlayer::new(video).into() + LauncherBackground::Video(video) }, Err(err) => { panic!("{:#?}", err) @@ -183,20 +206,19 @@ fn get_game_background(game: &PossibleGames) -> Element { .unwrap() .decode() .unwrap(); - let handle = image::Handle::from_rgba( + LauncherBackground::Image(image::Handle::from_rgba( img.width(), img.height(), img.to_rgba8().into_raw() - ); - image(handle).content_fit(iced::ContentFit::Fill).into() + )) } } else { - panic!("Missing icon for {:?}, path: {}", game, file_path) + panic!("Missing icon for {:?}, path: {}", state.selected_game, file_path) } } -fn get_game_icon(game: &PossibleGames) -> Element { +fn get_game_icon_handle(game: &PossibleGames) -> image::Handle { let file_path: &str = match game { PossibleGames::WutheringWaves => "wutheringwaves-icon.png", PossibleGames::ZenlessZoneZero => "zenlesszonezero-icon.png", @@ -208,24 +230,22 @@ fn get_game_icon(game: &PossibleGames) -> Element { let img = round_image(data_cursor) .unwrap() .resize(126, 126, ::image::imageops::FilterType::Lanczos3); - let handle = image::Handle::from_rgba( + + image::Handle::from_rgba( img.width(), img.height(), img.to_rgba8().into_raw() - ); - container(image(handle).content_fit(iced::ContentFit::Contain).height(Length::Fixed(64.0)).filter_method(image::FilterMethod::Linear)) - .style(move |_| { - container::Style { - border: border::rounded(20), - ..container::Style::default() - } - }) - .into() + ) } else { panic!("Missing icon for {:?}, path: {}", game, file_path) } } +fn get_game_icon<'a>(state: &'a State, game: &'a PossibleGames) -> Element<'a, Message> { + let handle = state.icon_images.get(game).unwrap(); + container(image(handle).content_fit(iced::ContentFit::Contain).height(Length::Fixed(64.0)).filter_method(image::FilterMethod::Linear)).into() +} + fn style_container(direction: f32, use_gradient: bool) -> container::Style { let angle = rad(direction); let gradient: Option = if use_gradient { @@ -241,7 +261,18 @@ fn style_container(direction: f32, use_gradient: bool) -> container::Style { } impl Launcher { fn boot() -> (Self, Task) { - (Self::Loaded(State::default()), Task::none()) + let launcher_bg = get_game_background(&State::default()); + let mut icons = HashMap::new(); + for game in PossibleGames::iter() { + let icon = get_game_icon_handle(&game); + icons.insert(game, icon); + } + let final_state = State { + background: Some(launcher_bg), + icon_images: icons, + ..State::default() + }; + (Self::Loaded(Box::new(final_state)), Task::none()) } fn title(&self) -> String { @@ -251,8 +282,8 @@ impl Launcher { fn update(&mut self, message: Message) -> Task { match self { Launcher::Loading => match message { - Message::Loaded(Ok(state)) => { - *self = Launcher::Loaded(state); + Message::Loaded(Ok(save_state)) => { + *self = Launcher::Loaded(save_state.into()); Task::none() }, _ => Task::none(), @@ -270,26 +301,26 @@ impl Launcher { } } - fn view(&self) -> Element { - let game_selector = container( - row![ - get_game_icon(&PossibleGames::WutheringWaves), - get_game_icon(&PossibleGames::ZenlessZoneZero), - get_game_icon(&PossibleGames::HonkaiStarRail), - get_game_icon(&PossibleGames::GenshinImpact), - ] - .spacing(10), - ) - .padding(10) - .align_y(Top) - .align_x(Center) - .width(Length::Fill) - .style(move |_| style_container(0.0, true)); - - println!("whuh"); + fn view(&self) -> Element { + println!("rerender triggered"); match self { Launcher::Loading => center(text("Loading...").size(50)).into(), Launcher::Loaded(state) => { + let game_selector = container( + row![ + get_game_icon(state, &PossibleGames::WutheringWaves), + get_game_icon(state, &PossibleGames::ZenlessZoneZero), + get_game_icon(state, &PossibleGames::HonkaiStarRail), + get_game_icon(state, &PossibleGames::GenshinImpact), + ] + .spacing(10), + ) + .padding(10) + .align_y(Top) + .align_x(Center) + .width(Length::Fill) + .style(move |_| style_container(0.0, true)); + let topbar = container( mouse_area(row![ text("Reversed Rooms").size(25), @@ -322,8 +353,13 @@ impl Launcher { column![topbar, Space::new(Length::Fill, Length::Fill), bottom_bar].width(Length::Fill); let content = container(user_area).center(Length::Fill); + let background = state.background.as_ref().unwrap(); + let bg_element: Element = match background { + LauncherBackground::Video(video) => VideoPlayer::new(video).into(), + LauncherBackground::Image(handle) => image(handle.clone()).into(), + }; - stack![get_game_background(&state.selected_game), game_selector, content].into() + stack![bg_element, game_selector, content].into() } } }