update
This commit is contained in:
parent
c7ee569790
commit
b38351c821
8 changed files with 308 additions and 273 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue