ui
All checks were successful
ci/woodpecker/push/build Pipeline was successful

This commit is contained in:
JMARyA 2024-12-14 00:24:10 +01:00
parent 2d2f7c5522
commit 4a4ae89b1b
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
6 changed files with 238 additions and 22 deletions

View file

@ -1,5 +1,6 @@
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
use walkdir::WalkDir; use walkdir::WalkDir;
use func::is_video_file; use func::is_video_file;
@ -96,7 +97,7 @@ impl Library {
pub async fn get_video_by_id(&self, id: &str) -> Option<Video> { pub async fn get_video_by_id(&self, id: &str) -> Option<Video> {
sqlx::query_as("SELECT * FROM videos WHERE id = $1") sqlx::query_as("SELECT * FROM videos WHERE id = $1")
.bind(id) .bind(uuid::Uuid::from_str(id).unwrap())
.fetch_optional(&self.conn) .fetch_optional(&self.conn)
.await .await
.unwrap() .unwrap()

View file

@ -76,7 +76,9 @@ async fn launch() -> _ {
pages::index::channel_page, pages::index::channel_page,
pages::yt::yt_tags, pages::yt::yt_tags,
pages::yt::yt_tag_page, pages::yt::yt_tag_page,
pages::yt::yt_channel_page pages::yt::yt_channel_page,
pages::index::index_page,
pages::watch::watch_page
], ],
) )
.attach(cors) .attach(cors)

View file

@ -1,20 +1,66 @@
use core::num;
use maud::{html, PreEscaped}; use maud::{html, PreEscaped};
use crate::library::Video; use crate::library::Video;
use rocket::{
http::{ContentType, Status},
request::{self, FromRequest, Request},
};
pub struct HTMX {
is_htmx: bool,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for HTMX {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
rocket::outcome::Outcome::Success(HTMX {
is_htmx: !req
.headers()
.get("HX-Request")
.collect::<Vec<&str>>()
.is_empty(),
})
}
}
pub fn shell(content: PreEscaped<String>, title: &str) -> PreEscaped<String> { pub fn shell(content: PreEscaped<String>, title: &str) -> PreEscaped<String> {
html! { html! {
html { html {
head { head {
title { (title) }; title { (title) };
script src="https://cdn.tailwindcss.com" {};
script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous" {};
meta name="viewport" content="width=device-width, initial-scale=1.0";
}; };
body { body class="bg-black text-white" {
div id="main_content" {
(content) (content)
};
} }
} }
} }
} }
pub fn render_page(
htmx: HTMX,
content: PreEscaped<String>,
title: &str,
) -> (Status, (ContentType, String)) {
if !htmx.is_htmx {
(
Status::Ok,
(ContentType::HTML, shell(content, title).into_string()),
)
} else {
(Status::Ok, (ContentType::HTML, content.into_string()))
}
}
pub fn loading_spinner() -> PreEscaped<String> { pub fn loading_spinner() -> PreEscaped<String> {
html! { html! {
style { style {
@ -36,17 +82,77 @@ pub fn search_bar(query: &str) -> PreEscaped<String> {
} }
} }
fn format_seconds_to_hhmmss(seconds: f64) -> String {
let total_seconds = seconds as u64;
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
if hours != 0 {
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
} else {
format!("{:02}:{:02}", minutes, seconds)
}
}
pub fn format_date(date: &chrono::NaiveDate) -> String {
// TODO : Implement
date.to_string()
}
pub fn format_number(num: i64) -> String {
// TODO : Implement
num.to_string()
}
pub async fn video_element_wide(video: &mut 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" {
div class="flex-shrink-0" {
img width="480" src=(format!("/video/thumbnail?v={}", video.id)) alt="Video Thumbnail" class="w-48 h-32 object-cover rounded-md";
};
div class="flex flex-col flex-grow ml-4" {
h3 class="text-lg font-semibold truncate mb-1" {
( video.title )
};
@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) ) }
};
div class="text-sm text-gray-400" {
span { ( format_number(meta.views) ) }
span { " views" }
span { " - " }
span class="ml-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> { pub async fn video_element(video: &mut Video) -> PreEscaped<String> {
html!( html!(
@let desc = video.youtube_meta().await.map(|x| x.description); a href=(format!("/watch?v={}", video.id)) class="max-w-sm mx-auto p-4 max-h-60 aspect-video" {
@let video_id = video.id; div class="bg-gray-900 shadow-lg rounded-lg overflow-hidden" {
article class="container-fluid" style="margin: 50px; cursor: pointer;" { div class="relative" {
a href=(format!("/watch?v={video_id}")) style="text-decoration:none !important;" { img width="480" src=(format!("/video/thumbnail?v={}", video.id)) alt="Video Thumbnail" class="w-full h-auto object-cover aspect-video";
img style="width: 350px;" width="480" src=(format!("/video/thumbnail?v={video_id}")); span class="absolute bottom-2 right-2 bg-black text-white text-xs px-2 py-1 rounded-sm opacity-90" {
div style="padding: 10px;" { (( format_seconds_to_hhmmss(video.duration) ))
h2 style="margin: 0; font-size: 18px;" { (video.title) }; };
@if !desc.as_ref().map(|x| x.is_empty()).unwrap_or(true) { };
p style="margin: 0; color: grey; font-size: 14px;margin-top: 10px;" { (desc.unwrap().chars().take(200).chain("...".to_string().chars()).take(203).collect::<String>()) };
div class="bg-gray-900 shadow-lg rounded-lg overflow-hidden" {
div class="p-4" {
h3 class="text-lg font-semibold truncate" {
( video.title )
};
}; };
}; };
}; };

View file

@ -1,10 +1,18 @@
use maud::html; use maud::html;
use rocket::{get, State}; use rocket::{
get,
http::{ContentType, Status},
State,
};
use serde_json::json; use serde_json::json;
use crate::library::Library; use crate::{library::Library, pages::components::video_element};
use super::vec_to_api; use super::{
api_response,
components::{render_page, video_element_wide, HTMX},
vec_to_api,
};
#[get("/search?<query>&<offset>")] #[get("/search?<query>&<offset>")]
pub async fn search( pub async fn search(
@ -23,13 +31,49 @@ pub async fn search(
} }
#[get("/d/<dir>")] #[get("/d/<dir>")]
pub async fn channel_page(dir: &str, library: &State<Library>) -> Option<serde_json::Value> { pub async fn channel_page(
let mut dir_videos = library.get_directory_videos(dir).await; htmx: HTMX,
dir: &str,
library: &State<Library>,
) -> (Status, (ContentType, String)) {
if dir.ends_with(".json") {
let dir_videos = library
.get_directory_videos(dir.split_once(".json").map(|x| x.0).unwrap_or_default())
.await;
return api_response(&json!(vec_to_api(&dir_videos).await));
}
Some(json!(vec_to_api(&mut dir_videos).await)) 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" {
@for mut vid in dir_videos {
( video_element_wide(&mut vid).await );
};
};
);
render_page(htmx, content, dir)
} }
#[get("/")] #[get("/")]
pub async fn index_page(library: &State<Library>) -> String { pub async fn index_page(htmx: HTMX, library: &State<Library>) -> (Status, (ContentType, String)) {
unimplemented!() let content = html!(
h1 class="text-center text-4xl font-extrabold leading-tight mt-4" { "Random Videos" };
div class="grid grid-cols-3 gap-6 p-6" {
@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-8" { "Directories:" };
div class="flex p-10" {
@for dir in library.get_directories().await {
a class="px-3 py-2 m-2 bg-purple-500 text-white rounded-full cursor-pointer hover:bg-purple-600" href=(format!("/d/{dir}")) { (dir) };
br;
};
};
);
render_page(htmx, content, "WatchDogs")
} }

View file

@ -1,6 +1,9 @@
use rocket::http::{ContentType, Status};
pub mod assets; pub mod assets;
pub mod components; pub mod components;
pub mod index; pub mod index;
pub mod watch;
pub mod yt; pub mod yt;
/// A trait to generate a Model API representation in JSON format. /// A trait to generate a Model API representation in JSON format.
@ -19,3 +22,10 @@ pub async fn vec_to_api(items: &[impl ToAPI]) -> Vec<serde_json::Value> {
ret ret
} }
pub fn api_response(json: &serde_json::Value) -> (Status, (ContentType, String)) {
(
Status::Ok,
(ContentType::JSON, serde_json::to_string(json).unwrap()),
)
}

53
src/pages/watch.rs Normal file
View file

@ -0,0 +1,53 @@
use maud::html;
use rocket::{
get,
http::{ContentType, Status},
State,
};
use serde_json::json;
use crate::{
library::{self, Library},
pages::components::video_element,
};
use super::{
components::{render_page, HTMX},
vec_to_api,
};
#[get("/watch?<v>")]
pub async fn watch_page(
htmx: HTMX,
library: &State<Library>,
v: String,
) -> (Status, (ContentType, String)) {
let video = if let Some(video) = library.get_video_by_id(&v).await {
video
} else {
// TODO : Error handling
library.get_video_by_youtube_id(&v).await.unwrap()
};
let content = html!(
main class="container mx-auto mt-6 flex flex-col lg:flex-row gap-6" {
div class="lg:w-2/3 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)) {
"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) };
};
};
};
);
render_page(htmx, content, &format!("{} - WatchDogs", video.title))
}