diff --git a/Cargo.lock b/Cargo.lock index 4df7fb9..3a9e358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,7 +146,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "based" version = "0.1.0" -source = "git+https://git.hydrar.de/jmarya/based?branch=ui#e02def6bc16dfe61937816d669514c9d4300ae1a" +source = "git+https://git.hydrar.de/jmarya/based?branch=ui#5ef37275ec504dc2e406d8feadca2e388e8d7fc9" dependencies = [ "bcrypt", "chrono", @@ -1163,13 +1163,13 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1989,9 +1989,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.8.0", "errno", diff --git a/Cargo.toml b/Cargo.toml index 21d8142..702ac41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,4 +24,4 @@ maud = "0.26.0" rand = "0.8.5" data-encoding = "2.6.0" bcrypt = "0.16.0" -based = { git = "https://git.hydrar.de/jmarya/based", features = ["cache", "htmx"], branch = "ui" } +based = { git = "https://git.hydrar.de/jmarya/based", features = ["cache"], branch = "ui" } diff --git a/src/main.rs b/src/main.rs index a717da1..5c34c03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use based::{auth::User, get_pg}; +use based::{asset::AssetRoutes, auth::User, get_pg}; use rocket::{http::Method, routes}; use std::path::Path; mod library; @@ -51,15 +51,15 @@ async fn launch() -> _ { .expect("error creating CORS options"); rocket::build() + .mount_assets() .mount( "/", routes![ - based::htmx::htmx_script_route, pages::assets::video_file, pages::assets::video_thumbnail, pages::assets::fav_icon, pages::index::search, - pages::index::channel_page, + pages::index::dir_page, pages::yt::yt_tags, pages::yt::yt_tag_page, pages::yt::yt_channel_page, diff --git a/src/pages/components.rs b/src/pages/components.rs index 8cdc54e..b8d9dae 100644 --- a/src/pages/components.rs +++ b/src/pages/components.rs @@ -1,5 +1,5 @@ use crate::library::Video; -use based::ui::components::AppBar; +use based::ui::components::{AppBar, Shell}; use based::ui::wrapper::HoverWrapper; use based::ui::{prelude::*, UIWidget}; use based::{ @@ -15,84 +15,164 @@ pub async fn render_page( title: &str, user: Option, ) -> StringResponse { - based::ui::render_page( - content, - title, - ctx, - &based::ui::components::Shell::new( - html! { - script src="https://cdn.tailwindcss.com" {}; - script src="/assets/htmx.min.js" {}; - meta name="viewport" content="width=device-width, initial-scale=1.0"; - }, - html! { - (script(include_str!("../scripts/header.js"))); - (AppBar("WatchDogs", user)) - }, - Some(String::from("bg-black text-white")), - ), + Shell::new( + Nothing(), + Div() + .vanish() + .push(script(include_str!("../scripts/header.js"))) + .push(AppBar("WatchDogs", user)), + Background(Text("").white()).color(Colors::Black), ) + .use_ui() + .render_page(content, title, ctx) .await } #[allow(non_snake_case)] pub fn HoverScaleAnimation(inner: T) -> HoverWrapper { - Hover(Animated(ZIndex::five(Scale(1.02, Nothing()))).scope(Scope::All)).on(inner) + Hover(Animated(ZIndex::Five(Scale(1.02, Nothing()))).scope(Scope::All)).on(inner) +} + +#[allow(non_snake_case)] +pub fn Title(title: T) -> PreEscaped { + Margin( + Paragraph(title) + .align(TextAlignment::Center) + ._4xl() + .extrabold() + .line_height(LineHeight::Tight), + ) + .top(ScreenValue::_6) + .bottom(ScreenValue::_2) + .render() +} + +#[allow(non_snake_case)] +pub fn VerticalVideoGrid(inner: T) -> PreEscaped { + Screen::large(Grid(Nothing()).columns(GridAmount::_3).gap(ScreenValue::_6)) + .on(Padding(Margin(inner).bottom(ScreenValue::_4)).all(ScreenValue::_6)) + .render() } pub async fn video_element_wide(video: &Video) -> PreEscaped { - html!( - a href=(format!("/watch?v={}", video.id)) class="flex items-center w-full my-4 bg-gray-900 hover:bg-gray-800 shadow-lg rounded-lg overflow-hidden" { - div class="flex-shrink-0 relative" { - img width="480" src=(format!("/video/thumbnail?v={}", video.id)) class="aspect-video h-32 w-48 rounded-l-md object-cover"; - span class="absolute bottom-2 right-2 bg-black text-white text-xs px-2 py-1 rounded-sm opacity-90" { - (( format_seconds_to_hhmmss(video.duration) )) - }; - }; + let yt_meta = video.youtube_meta().await; + Flex(Margin(Width(ScreenValue::full, + Hover(Background(Nothing()).color(Gray::_800)).on( + Background( + Rounded( + Shadow::large( + Link(&format!("/watch?v={}", video.id), + Div().vanish().push( + FlexGrow(Strategy::NoShrink, + Position(PositionKind::Relative, + Div().vanish().push( + html! { + img width="480" src=(format!("/video/thumbnail?v={}", video.id)) class="aspect-video h-32 w-48 rounded-l-md object-cover"; + } + ).push( + Position(PositionKind::Absolute, + Background( + Rounded( + Opacity(0.90, + Padding( + Span( +&format_seconds_to_hhmmss(video.duration) + ).white().xs() + ).x(ScreenValue::_2).y(ScreenValue::_1) + ) + ).size(Size::Small) + ).color(Colors::Black) + ).bottom(8).right(8) + ) + ) + ) + ).push( + Flex( + FlexGrow(Strategy::Grow, + Margin( + MaxHeight(ScreenValue::_32, + Div().vanish().push( + Margin(Text(&video.title).large().semibold().max_lines(LineClamp::_3).title(&video.title).align(TextAlignment::Start) + ).bottom(ScreenValue::_1) + ).push_some(yt_meta, |meta| { + Flex( + FlexGrow(Strategy::Grow, + Margin( + Padding( + Div().vanish().push( + Paragraph( + Margin( + Div().vanish().push( + Span(&meta.uploader_name).medium() + ).push( + Span(" - ") + ).push( + Span(&format_date(&meta.upload_date)) + ) + ).bottom(ScreenValue::_2) + ).sm().color(&Gray::_400).align(TextAlignment::Start) + ).push( + Paragraph( + Div().vanish().push( + Span(&format_number(meta.views)) + ).push( + Span(" views") + ) + ).sm().color(&Gray::_400).align(TextAlignment::Start) + ) + ).y(ScreenValue::_1) + ).left(ScreenValue::_2) + ) + ).direction(Direction::Column) + }) + ) + ).left(ScreenValue::_2) + ) + ).direction(Direction::Column) + ) - div class="flex flex-col flex-grow ml-2 max-h-32" { - (Margin(Text(&video.title).large().semibold().max_lines(LineClamp::_3).title(&video.title).align(TextAlignment::Start) - ).bottom(ScreenValue::_1) - ) + ))).size(Size::Large)).color(Gray::_900)) - div class="flex flex-col flex-grow ml-2 py-1" { - @if let Some(meta) = video.youtube_meta().await { - div class="text-sm text-gray-400 mb-2 text-start" { - (Span(&meta.uploader_name).medium()) - (Span(" - ")) - (Span(&format_date(&meta.upload_date))) - }; - div class="text-sm text-gray-400 text-start" { - (Span(&format_number(meta.views))) - (Span(" views")) - }; - }; - } - }; - }; - ) +)).y(ScreenValue::_4)).items_center().render() } pub fn video_thumbnail_with_time(video: &Video) -> PreEscaped { - html! { - div class="relative" { - ( - Width(ScreenValue::full, - Height(ScreenValue::auto, - Aspect::video( - Rounded( - Image(&format!("/video/thumbnail?v={}", video.id)) - ).size(Size::Large).side(Side::Top) - ) + Position( + PositionKind::Relative, + Div() + .push(Context(Width( + ScreenValue::full, + Height( + ScreenValue::auto, + Aspect::Video( + Rounded(Image(&format!("/video/thumbnail?v={}", video.id))) + .size(Size::Large) + .side(Side::Top), + ), + ), + ))) + .push(Context( + Position( + PositionKind::Absolute, + Background( + Padding( + Rounded(Opacity( + 0.90, + Span(&format_seconds_to_hhmmss(video.duration)).white().xs(), + )) + .size(Size::Small), ) + .x(ScreenValue::_2) + .y(ScreenValue::_1), ) + .color(Colors::Black), ) - span class="absolute bottom-2 right-2 bg-black text-white text-xs px-2 py-1 rounded-sm opacity-90" { - (( format_seconds_to_hhmmss(video.duration) )) - }; - }; - } + .bottom(8) + .right(8), + )), + ) + .render() } pub async fn video_element(video: &mut Video) -> PreEscaped { @@ -101,7 +181,7 @@ pub async fn video_element(video: &mut Video) -> PreEscaped { ScreenValue::_90, MaxHeight( ScreenValue::_60, - Aspect::video(Link( + Aspect::Video(Link( &format!("/watch?v={}", video.id), Context( Hover(Background(Nothing()).color(Gray::_800)).on(Background( diff --git a/src/pages/index.rs b/src/pages/index.rs index 6cbcd20..256c266 100644 --- a/src/pages/index.rs +++ b/src/pages/index.rs @@ -7,10 +7,11 @@ use based::{ RequestContext, StringResponse, }, }; -use maud::html; +use maud::{html, Render}; use rocket::{get, State}; use serde_json::json; +use crate::pages::components::{Title, VerticalVideoGrid}; use crate::{library::Library, pages::components::video_element}; use super::{ @@ -48,21 +49,23 @@ pub async fn latest_page( user: MaybeUser, ) -> StringResponse { let videos = library.get_newly_added(20).await; - - let content = html!( - h1 class="text-center text-4xl font-extrabold leading-tight mt-6 mb-2" { "Recent videos" }; - div class="p-6" { + let video_elements = html! { @for mut vid in videos { ( video_element_wide(&mut vid).await ); }; }; - ); + + let content = Div() + .vanish() + .push(Title("Recent videos")) + .push(Padding(video_elements).all(ScreenValue::_6)) + .render(); render_page(ctx, content, "Recent videos", user.into()).await } #[get("/d/")] -pub async fn channel_page( +pub async fn dir_page( ctx: RequestContext, dir: &str, library: &State, @@ -76,15 +79,17 @@ pub async fn channel_page( } let dir_videos = library.get_directory_videos(dir).await; - - let content = html!( - h1 class="text-center text-4xl font-extrabold leading-tight mt-4 mb-2" { (dir) }; - div class="p-6" { + let video_elements = html! { @for mut vid in dir_videos { ( video_element_wide(&mut vid).await ); }; }; - ); + + let content = Div() + .vanish() + .push(Title(dir.to_string())) + .push(Padding(video_elements).all(ScreenValue::_6)) + .render(); render_page(ctx, content, dir, user.into()).await } @@ -95,42 +100,49 @@ pub async fn index_page( library: &State, user: MaybeUser, ) -> StringResponse { - let content = html!( - h1 class="text-center text-4xl font-extrabold leading-tight mt-8" { "Random Videos" }; - div class="lg:grid grid-cols-3 gap-6 p-6 mb-4" { + let random_video_elements = html! { @for mut vid in library.get_random_videos(3).await { ( video_element(&mut vid).await ); }; - }; - - h1 class="text-center text-4xl font-extrabold leading-tight mt-16" { a href="/latest" { "Latest Videos" };}; - div class="lg:grid grid-cols-3 gap-6 p-6 mb-4" { + }; + let newly_added_elements = html! { @for mut vid in library.get_newly_added(3).await { ( video_element(&mut vid).await ); }; - }; - - h1 class="text-center text-4xl font-extrabold leading-tight mt-8" { "Directories:" }; - div class="flex flex-wrap p-10" { - @for dir in library.get_directories().await { - - - (Margin( - Padding( - Hover(Background(Cursor::Pointer.on(Nothing())).color(Purple::_600)).on( - Background( - Rounded( - Link( - &format!("/d/{dir}"), - Text(&dir).white() - ).use_htmx() - ).size(Size::Full) - ).color(Purple::_500)) - ).x(ScreenValue::_3).y(ScreenValue::_2)).all(ScreenValue::_2)) - br; - }; }; - ); + let directories = library.get_directories().await; + + let content = Div() + .vanish() + .push(Title("Random Videos")) + .push(VerticalVideoGrid(random_video_elements)) + .push(Title(Link("/latest", "Latest Videos").use_htmx())) + .push(VerticalVideoGrid(newly_added_elements)) + .push(Title("Directories:")) + .push( + Padding( + Flex(Div().vanish().push_for_each(&directories, |dir: &_| { + Margin( + Padding( + Hover(Background(Cursor::Pointer.on(Nothing())).color(Purple::_600)) + .on(Background( + Rounded( + Link(&format!("/d/{dir}"), Text(&dir).white()).use_htmx(), + ) + .size(Size::Full), + ) + .color(Purple::_500)), + ) + .x(ScreenValue::_3) + .y(ScreenValue::_2), + ) + .all(ScreenValue::_2) + })) + .wrap(Wrap::Wrap), + ) + .all(ScreenValue::_10), + ) + .render(); render_page(ctx, content, "WatchDogs", user.into()).await } diff --git a/src/pages/user.rs b/src/pages/user.rs index 1fd4ec3..4bee7e4 100644 --- a/src/pages/user.rs +++ b/src/pages/user.rs @@ -2,12 +2,12 @@ use based::auth::{Sessions, User}; use based::request::StringResponse; use based::ui::prelude::*; use based::{auth::MaybeUser, request::RequestContext}; -use maud::html; +use maud::{html, Render}; use rocket::http::CookieJar; use rocket::{form::Form, get, http::Cookie, post, response::Redirect, FromForm}; use crate::library::history::VideoHistory; -use crate::pages::components::video_element_wide; +use crate::pages::components::{video_element_wide, Title}; use super::components::render_page; @@ -50,14 +50,17 @@ pub async fn login_post(login_form: Form, cookies: &CookieJar<'_>) -> #[get("/history")] pub async fn history_page(ctx: RequestContext, user: User) -> StringResponse { - let content = html! { - h1 class="text-center text-4xl font-extrabold leading-tight mt-4 mb-2" { "History" }; - (Padding(html! { - @for mut vid in user.history_of(10).await { - ( video_element_wide(&mut vid).await ); - }; - }).all(ScreenValue::_6)) + let video_elements = html! { + @for mut vid in user.history_of(10).await { + ( video_element_wide(&mut vid).await ); + }; }; + let content = Div() + .vanish() + .push(Title("History")) + .push(Padding(video_elements).all(ScreenValue::_6)) + .render(); + render_page(ctx, content, "History", Some(user)).await } diff --git a/src/pages/watch.rs b/src/pages/watch.rs index c6358e3..efb85c7 100644 --- a/src/pages/watch.rs +++ b/src/pages/watch.rs @@ -9,7 +9,6 @@ use maud::{html, PreEscaped, Render}; use rocket::{get, State}; use crate::library::Video; -use crate::yt_meta::YouTubeMeta; use crate::{ library::{history::VideoHistory, Library}, pages::components::video_element_wide, @@ -44,22 +43,20 @@ pub async fn watch_page( Flex( Div().vanish() .push( - Margin( Screen::large(Width(Fraction::_10on12, Nothing())).on( Div().push( - Context(Aspect::video( + Context(Aspect::Video( Background( Rounded( - html! { - video - controls - autoplay - class="w-full h-full rounded-lg" { - source src=(format!("/video/raw?v={}", video.id)) type="video/mp4" { - "Your browser does not support the video" - }; - }; - } + Width(ScreenValue::full, + Height(ScreenValue::full, + Rounded( + Video().controls().autoplay().add_src( + Source(&format!("/video/raw?v={}", video.id), Some("video/mp4".to_string())) + ) + ).size(Size::Large) + ) + ) ).size(Size::Large) ).color(Colors::Black) )) @@ -94,15 +91,14 @@ pub async fn watch_page( ) )).size(Size::Large) ).color(Stone::_900) - ).all(ScreenValue::_4)).top(ScreenValue::_8)) + ).all(ScreenValue::_4)).top(ScreenValue::_4)) ) ) - ).top(ScreenValue::_10) ).push( rec ) ).direction(Direction::Column).gap(ScreenValue::_6)) - ).x(ScreenValue::auto).top(ScreenValue::_6) + ).x(ScreenValue::_10).top(ScreenValue::_6) ).render(); render_page(