This commit is contained in:
JMARyA 2025-01-17 19:35:35 +01:00
parent c7ee569790
commit b38351c821
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
8 changed files with 308 additions and 273 deletions

View file

@ -254,7 +254,10 @@ impl Library {
for path in lib {
if !Video::has_path(&path.display().to_string()).await {
Video::insert_path_to_db(&path).await;
if let Some(video) = Video::insert_path_to_db(&path).await {
// Ensure thumbnail
self.get_thumbnail(&video).await;
}
}
}

View file

@ -35,12 +35,6 @@ async fn launch() -> _ {
library
.scan_dir(&Path::new(&dir_path.clone()).to_path_buf())
.await;
// Ensure thumbnails
for video in library.get_all_videos().await {
log::info!("Ensure thumbnail for {} [{}]", video.title, video.id);
library.get_thumbnail(&video).await;
}
});
let cors = rocket_cors::CorsOptions {

View file

@ -1,11 +1,12 @@
use crate::library::Video;
use based::ui::components::AppBar;
use based::ui::prelude::*;
use based::{
auth::User,
format::{format_date, format_number, format_seconds_to_hhmmss},
page::script,
request::{RequestContext, StringResponse},
};
use maud::{html, PreEscaped};
use maud::{html, PreEscaped, Render};
pub async fn render_page(
ctx: RequestContext,
@ -13,33 +14,19 @@ pub async fn render_page(
title: &str,
user: Option<User>,
) -> StringResponse {
based::page::render_page(
based::ui::render_page(
content,
title,
ctx,
&based::page::Shell::new(
&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! {
header class="bg-gray-800 text-white shadow-md py-2" {
(script(include_str!("../scripts/header.js")));
div class="flex justify-between px-6" {
a href="/" class="flex items-center space-x-2" {
img src="/favicon" alt="Logo" class="w-10 h-10 rounded-md";
span class="font-semibold text-xl" { "WatchDogs" };
};
@if user.is_some() {
p { (user.unwrap().username) };
};
};
};
(script(include_str!("../scripts/header.js")));
(AppBar("WatchDogs", user))
},
Some(String::from("bg-black text-white")),
),
@ -47,52 +34,31 @@ pub async fn render_page(
.await
}
pub fn loading_spinner() -> PreEscaped<String> {
html! {
style {
".spinner { display: flex;justify-content: center;align-items: center;height: 100vh;}
.spinner-border { border: 2px solid #007bff;border-top: 2px solid transparent;border-radius: 50%;width: 40px;height: 40px;animation: spin 1s linear infinite;}
@keyframes spin {0% { transform: rotate(0deg); }100% { transform: rotate(360deg); }}"
};
div class="spinner" {
div class="spinner-border" {};
};
}
}
pub fn search_bar(query: &str) -> PreEscaped<String> {
html! {
form hx-get="/search" action="/search" hx-push-url="true" hx-target="#main-view" hx-swap="innerHTML" {
input style="width: 100%;" value=(query) name="query" type="search" placeholder="Search...";
};
}
}
pub async fn video_element_wide(video: &Video) -> PreEscaped<String> {
html!(
a href=(format!("/watch?v={}", video.id)) class="flex items-center w-full p-4 bg-gray-900 shadow-lg rounded-lg overflow-hidden mb-2 mt-2" {
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="w-48 h-32 object-cover rounded-md";
img width="480" src=(format!("/video/thumbnail?v={}", video.id)) class="aspect-video w-32 w-48 rounded-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) ))
};
};
div class="flex flex-col flex-grow ml-4" {
h3 class="text-lg font-semibold mb-1" title=(video.title) {
( video.title )
};
div class="flex flex-col flex-grow ml-2" {
(Margin(Text(&video.title).large().semibold().max_lines(LineClamp::_3).title(&video.title).align(TextAlignment::Start)
).bottom(ScreenValue::_1)
)
@if let Some(meta) = video.youtube_meta().await {
div class="text-sm text-gray-400 mb-2" {
span class="font-medium" { ( meta.uploader_name ) }
span { " - " }
span { ( format_date(&meta.upload_date) ) }
(Span(&meta.uploader_name).medium())
(Span(" - "))
(Span(&format_date(&meta.upload_date)))
};
div class="text-sm text-gray-400" {
span { ( format_number(meta.views) ) }
span { " views" }
(Span(&format_number(meta.views)))
(Span(" views"))
};
};
};
@ -100,25 +66,53 @@ pub async fn video_element_wide(video: &Video) -> PreEscaped<String> {
)
}
pub fn video_thumbnail_with_time(video: &Video) -> PreEscaped<String> {
html! {
div class="relative" {
img width="480" src=(format!("/video/thumbnail?v={}", video.id)) class="w-full h-auto object-cover aspect-video";
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) ))
};
};
}
}
pub async fn video_element(video: &mut Video) -> PreEscaped<String> {
html!(
a href=(format!("/watch?v={}", video.id)) class="max-w-sm mx-auto p-4 max-h-60 aspect-video" {
div class="bg-gray-900 shadow-lg rounded-lg overflow-hidden" {
div class="relative" {
img width="480" src=(format!("/video/thumbnail?v={}", video.id)) class="w-full h-auto object-cover aspect-video";
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) ))
};
};
div class="bg-gray-900 shadow-lg rounded-lg overflow-hidden" {
div class="p-4" {
h3 class="text-lg font-semibold" title=(video.title) {
( video.title )
};
};
};
};
};
)
Margin(Context(MaxWidth(
ScreenValue::_90,
MaxHeight(
ScreenValue::_60,
Aspect::video(Link(
&format!("/watch?v={}", video.id),
Context(
Hover(Background(Gray::_800, Nothing())).on(Background(
Gray::_900,
Shadow::large(
Rounded(
Div().push(video_thumbnail_with_time(&video)).push(Context(
Rounded(Shadow::large(
Padding(
Text(&video.title)
.base_size()
.semibold()
.align(TextAlignment::Start)
.title(&video.title)
.max_lines(LineClamp::_1)
.overflow(TextOverflow::Truncate),
)
.y(ScreenValue::_4)
.x(ScreenValue::_2),
))
.size(Size::Large),
)),
)
.size(Size::Large),
),
)),
),
)),
),
)))
.y(ScreenValue::_6)
.render()
}

View file

@ -1,6 +1,7 @@
use based::ui::htmx::HTMXAttributes;
use based::ui::prelude::*;
use based::{
auth::MaybeUser,
page::htmx_link,
request::{
api::{vec_to_api, FallibleApiResponse},
RequestContext, StringResponse,
@ -102,7 +103,7 @@ pub async fn index_page(
};
};
h1 class="text-center text-4xl font-extrabold leading-tight mt-10" { a href="/latest" { "Latest Videos" };};
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" {
@for mut vid in library.get_newly_added(3).await {
( video_element(&mut vid).await );
@ -112,7 +113,21 @@ pub async fn index_page(
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 {
(htmx_link(&format!("/d/{dir}"), "px-3 py-2 m-2 bg-purple-500 text-white rounded-full cursor-pointer hover:bg-purple-600", "", html! { (dir) }));
(Margin(
Padding(
Hover(Background(Purple::_600, Nothing())).on(
Background(Purple::_500,
Rounded(
// TODO : Implement cursor-pointer
Link(
&format!("/d/{dir}"),
Text(&dir).white()
).use_htmx()
).size(Size::Full)
))
).x(ScreenValue::_3).y(ScreenValue::_2)).all(ScreenValue::_2))
br;
};
};

View file

@ -1,5 +1,6 @@
use based::auth::{Sessions, User};
use based::request::StringResponse;
use based::ui::prelude::*;
use based::{auth::MaybeUser, request::RequestContext};
use maud::html;
use rocket::http::CookieJar;
@ -51,11 +52,11 @@ pub async fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) ->
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" };
div class="p-6" {
@for mut vid in user.history_of(10).await {
( video_element_wide(&mut vid).await );
};
};
(Padding(html! {
@for mut vid in user.history_of(10).await {
( video_element_wide(&mut vid).await );
};
}).all(ScreenValue::_6))
};
render_page(ctx, content, "History", Some(user)).await

View file

@ -1,11 +1,15 @@
use based::ui::primitives::space::Fraction;
use based::ui::{prelude::*, AttrExtendable};
use based::{
auth::MaybeUser,
format::format_date,
request::{RequestContext, StringResponse},
};
use maud::html;
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,
@ -31,43 +35,75 @@ pub async fn watch_page(
user.insert_history(video.id).await;
}
let content = html!(
main class="container mx-auto mt-6 flex flex-col lg:flex-row gap-6" {
div class="lg:w-10/12 mt-10" {
div class="bg-black aspect-video rounded-lg overflow-hidden" {
video
controls
autoplay
class="w-full h-full" {
source src=(format!("/video/raw?v={}", video.id)) type="video/mp4" {
"Your browser does not support the video"
let youtube_meta = video.youtube_meta().await;
let rec = build_rec(&library, &video).await;
let content = Container(
Margin(
Screen::large(Flex(Nothing()).direction(Direction::Row)).on(
Flex(
Div().vanish()
.push(
Margin(
Screen::large(Width(Fraction::_10on12, Nothing())).on(
Div().push(
Context(Aspect::video(
Background(Colors::Black,
Rounded(
html! {
video
controls
autoplay
class="w-full h-full" {
source src=(format!("/video/raw?v={}", video.id)) type="video/mp4" {
"Your browser does not support the video"
};
};
};
};
div class="p-4 bg-stone-900 rounded-lg shadow-lg mt-8" {
h2 class="text-2xl font-semibold" { (video.title) };
@if let Some(meta) = video.youtube_meta().await {
div class="flex justify-between mt-2" {
p class="mb-4 text-gray-300" { (meta.uploader_name) };
p class="mb-4 text-gray-300" { (format!("{} Views ﹣ {}", meta.views, format_date(&meta.upload_date))) };
};
a href=(format!("https://www.youtube.com/watch?v={}", meta.id)) class="text-blue-400" {"Watch on YouTube" };
p class="mb-2 text-gray-300 text-bold mt-2" { "Description: " } span { (meta.description) };
};
};
};
div id="recommendations" class="mt-8 w-1/3" {
h3 class="text-center text-4xl font-extrabold leading-tight mb-2" { "In " a class="text-blue-500" href=(format!("/d/{}", video.directory)) { (video.directory) }; }
@for video in library.get_directory_videos(&video.directory).await {
(video_element_wide(&video).await);
};
};
};
);
}
).size(Size::Large)
)
))
).push(
Context(Margin(Padding(
Background(Stone::_900,
Rounded(
Shadow::large(
Div()
.push(
Text(&video.title)._2xl().semibold()
)
.push_some(youtube_meta.as_ref(), |meta: &_| {
Div().vanish()
.push(
Margin(Flex(
Div().vanish().push(
Margin(Text(&format_date(&meta.upload_date)).color(&Gray::_300)).bottom(ScreenValue::_4)
).push(
Margin(Text(&format!("{} Views ﹣ {}", meta.views, format_date(&meta.upload_date))).color(&Gray::_300)).bottom(ScreenValue::_4)
)
).justify(Justify::Between).group()).top(ScreenValue::_2)
)
.push(
Link(&format!("https://www.youtube.com/watch?v={}", meta.id),
Text("Watch on YouTube").color(&Red::_400)
)
).push(
Margin(Text(&meta.description).bold().color(&Gray::_300).wrap(TextWrap::Pretty).whitespace(TextWhitespace::BreakSpaces)).bottom(ScreenValue::_2).top(ScreenValue::_2)
)
}
)
)).size(Size::Large)
)
).all(ScreenValue::_4)).top(ScreenValue::_8))
)
)
).top(ScreenValue::_10)
).push(
rec
)
).direction(Direction::Column).gap(ScreenValue::_6))
).x(ScreenValue::auto).top(ScreenValue::_6)
).render();
render_page(
ctx,
@ -77,3 +113,41 @@ pub async fn watch_page(
)
.await
}
pub async fn build_rec(library: &Library, video: &Video) -> PreEscaped<String> {
let videos = library.get_directory_videos(&video.directory).await;
let video_elements = html! {
@for video in videos {
(video_element_wide(&video).await);
};
};
Margin(Width(
Fraction::_1on3,
Div()
.id("recommendations")
.push(
Margin(
Paragraph(Context(
SpaceBetween(
Flex(Div().vanish().push(Span("In ")).push(Link(
&format!("/d/{}", video.directory),
Text(&video.directory).color(&Blue::_500),
)))
.group()
.justify(Justify::Center),
)
.x(ScreenValue::_2),
))
.align(TextAlignment::Center)
._4xl()
.extrabold(),
)
.bottom(ScreenValue::_2),
)
.push(video_elements),
))
.top(ScreenValue::_8)
.render()
}