This commit is contained in:
parent
2ac2559c23
commit
59dae90b4d
13 changed files with 368 additions and 138 deletions
12
src/config.rs
Normal file
12
src/config.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Config {
|
||||
pub general: GeneralConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct GeneralConfig {
|
||||
pub private: bool,
|
||||
pub video_path: String,
|
||||
}
|
28
src/main.rs
28
src/main.rs
|
@ -1,10 +1,17 @@
|
|||
use based::{asset::AssetRoutes, auth::User, get_pg};
|
||||
use based::{
|
||||
asset::AssetRoutes,
|
||||
auth::User,
|
||||
get_pg,
|
||||
ui::components::{AppBar, Shell},
|
||||
};
|
||||
use rocket::{http::Method, routes};
|
||||
use std::path::Path;
|
||||
mod config;
|
||||
mod library;
|
||||
mod meta;
|
||||
mod pages;
|
||||
mod yt_meta;
|
||||
use based::ui::prelude::*;
|
||||
|
||||
#[rocket::launch]
|
||||
async fn launch() -> _ {
|
||||
|
@ -12,14 +19,10 @@ async fn launch() -> _ {
|
|||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let conf: config::Config =
|
||||
toml::from_str(&std::fs::read_to_string("config.toml").unwrap()).unwrap();
|
||||
|
||||
if args.len() != 2 {
|
||||
eprintln!("Usage: {} <directory_path>", args[0]);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let dir_path = args[1].clone();
|
||||
let dir_path = conf.general.video_path.clone();
|
||||
|
||||
let pg = get_pg!();
|
||||
|
||||
|
@ -50,6 +53,13 @@ async fn launch() -> _ {
|
|||
.to_cors()
|
||||
.expect("error creating CORS options");
|
||||
|
||||
let shell = Shell::new(
|
||||
Nothing(),
|
||||
Nothing(),
|
||||
Background(Text("").white()).color(Colors::Black),
|
||||
)
|
||||
.use_ui();
|
||||
|
||||
rocket::build()
|
||||
.mount_assets()
|
||||
.mount(
|
||||
|
@ -74,4 +84,6 @@ async fn launch() -> _ {
|
|||
)
|
||||
.attach(cors)
|
||||
.manage(lib)
|
||||
.manage(conf)
|
||||
.manage(shell)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
use based::request::assets::DataResponse;
|
||||
use based::{auth::MaybeUser, request::assets::DataResponse};
|
||||
use rocket::{get, State};
|
||||
|
||||
use tokio::{fs::File, io::AsyncReadExt};
|
||||
|
||||
use crate::library::Library;
|
||||
use crate::{config::Config, library::Library};
|
||||
|
||||
#[get("/video/raw?<v>")]
|
||||
pub async fn video_file(v: &str, library: &State<Library>) -> Option<DataResponse> {
|
||||
pub async fn video_file(
|
||||
v: &str,
|
||||
library: &State<Library>,
|
||||
conf: &State<Config>,
|
||||
user: MaybeUser,
|
||||
) -> Option<DataResponse> {
|
||||
if conf.general.private && user.user().is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let video = if let Some(video) = library.get_video_by_id(v).await {
|
||||
video
|
||||
} else {
|
||||
|
@ -33,7 +42,16 @@ pub async fn video_file(v: &str, library: &State<Library>) -> Option<DataRespons
|
|||
}
|
||||
|
||||
#[get("/video/thumbnail?<v>")]
|
||||
pub async fn video_thumbnail(v: &str, library: &State<Library>) -> Option<DataResponse> {
|
||||
pub async fn video_thumbnail(
|
||||
v: &str,
|
||||
library: &State<Library>,
|
||||
conf: &State<Config>,
|
||||
user: MaybeUser,
|
||||
) -> Option<DataResponse> {
|
||||
if conf.general.private && user.user().is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let video = if let Some(video) = library.get_video_by_id(v).await {
|
||||
video
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use crate::library::Video;
|
||||
use based::ui::components::{AppBar, Shell};
|
||||
use based::ui::components::prelude::Avatar;
|
||||
use based::ui::components::prelude::*;
|
||||
use based::ui::components::{AppBar, Card, NavBar, Shell};
|
||||
use based::ui::primitives::flex::Column;
|
||||
use based::ui::primitives::Optional;
|
||||
use based::ui::wrapper::HoverWrapper;
|
||||
use based::ui::{prelude::*, UIWidget};
|
||||
use based::{
|
||||
|
@ -10,22 +14,37 @@ use based::{
|
|||
use maud::{html, PreEscaped, Render};
|
||||
|
||||
pub async fn render_page(
|
||||
shell: &Shell,
|
||||
ctx: RequestContext,
|
||||
content: PreEscaped<String>,
|
||||
title: &str,
|
||||
user: Option<User>,
|
||||
) -> StringResponse {
|
||||
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
|
||||
shell
|
||||
.extend()
|
||||
.with_navbar(
|
||||
NavBar("WatchDogs")
|
||||
.icon(Sized(
|
||||
ScreenValue::_8,
|
||||
ScreenValue::_8,
|
||||
Rounded(Image("/favicon").alt("Logo")).size(Size::Medium),
|
||||
))
|
||||
.extra(Optional(user.as_ref(), |user| {
|
||||
// TODO : profile pictures
|
||||
DropDown(
|
||||
Avatar("", &user.username).use_initials(),
|
||||
Column(vec![Link(
|
||||
"/history",
|
||||
Padding(Text("Video History").medium())
|
||||
.x(ScreenValue::_4)
|
||||
.y(ScreenValue::_2),
|
||||
)
|
||||
.use_htmx()]),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.render_page(content, title, ctx)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
|
@ -48,7 +67,18 @@ pub fn Title<T: UIWidget + 'static>(title: T) -> PreEscaped<String> {
|
|||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn VerticalVideoGrid<T: UIWidget + 'static>(inner: T) -> PreEscaped<String> {
|
||||
pub async fn VerticalVideoGrid(videos: &mut [Video]) -> PreEscaped<String> {
|
||||
let video_elements = html! {
|
||||
@for mut vid in videos {
|
||||
( video_element(&mut vid).await );
|
||||
};
|
||||
};
|
||||
|
||||
VerticalVideoGridRaw(video_elements)
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn VerticalVideoGridRaw<T: UIWidget + 'static>(inner: T) -> PreEscaped<String> {
|
||||
Screen::large(Grid(Nothing()).columns(GridAmount::_3).gap(ScreenValue::_6))
|
||||
.on(Padding(Margin(inner).bottom(ScreenValue::_4)).all(ScreenValue::_6))
|
||||
.render()
|
||||
|
@ -141,7 +171,7 @@ pub fn video_thumbnail_with_time(video: &Video) -> PreEscaped<String> {
|
|||
Position(
|
||||
PositionKind::Relative,
|
||||
Div()
|
||||
.push(Context(Width(
|
||||
.push(Context(ObjectFit::Cover(Width(
|
||||
ScreenValue::full,
|
||||
Height(
|
||||
ScreenValue::auto,
|
||||
|
@ -151,7 +181,7 @@ pub fn video_thumbnail_with_time(video: &Video) -> PreEscaped<String> {
|
|||
.side(Side::Top),
|
||||
),
|
||||
),
|
||||
)))
|
||||
))))
|
||||
.push(Context(
|
||||
Position(
|
||||
PositionKind::Absolute,
|
||||
|
@ -216,3 +246,19 @@ pub async fn video_element(video: &mut Video) -> PreEscaped<String> {
|
|||
)
|
||||
.render()
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn DirectoryBadge(dir: &str) -> PreEscaped<String> {
|
||||
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)
|
||||
.render()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use based::ui::htmx::HTMXAttributes;
|
||||
use based::page;
|
||||
use based::request::respond_html;
|
||||
use based::ui::components::prelude::InfinityScroll;
|
||||
use based::ui::components::{ColoredSpinner, Shell};
|
||||
use based::ui::prelude::*;
|
||||
use based::{
|
||||
auth::MaybeUser,
|
||||
|
@ -7,24 +10,42 @@ use based::{
|
|||
RequestContext, StringResponse,
|
||||
},
|
||||
};
|
||||
use maud::{html, Render};
|
||||
use rocket::{get, State};
|
||||
use maud::{html, PreEscaped, Render};
|
||||
use rocket::{get, uri, State};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::pages::components::{Title, VerticalVideoGrid};
|
||||
use crate::config::Config;
|
||||
use crate::library::Video;
|
||||
use crate::pages::components::{Title, VerticalVideoGrid, VerticalVideoGridRaw};
|
||||
use crate::{library::Library, pages::components::video_element};
|
||||
|
||||
use super::components::DirectoryBadge;
|
||||
use super::{
|
||||
api_response,
|
||||
components::{render_page, video_element_wide},
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! check_private {
|
||||
($conf:ident, $user:ident, $shell:ident, $ctx:ident) => {
|
||||
if $conf.general.private && $user.user().is_none() {
|
||||
return $crate::pages::index::is_private_page($shell, $ctx).await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[get("/search?<query>&<offset>")]
|
||||
pub async fn search(
|
||||
query: &str,
|
||||
offset: Option<i64>,
|
||||
library: &State<Library>,
|
||||
conf: &State<Config>,
|
||||
user: MaybeUser,
|
||||
shell: &State<Shell>,
|
||||
ctx: RequestContext,
|
||||
) -> Option<serde_json::Value> {
|
||||
// todo : check_private!(conf, user, shell, ctx);
|
||||
|
||||
const NUM_OF_RESULTS: i64 = 20;
|
||||
|
||||
// get start parameter for search result chunks
|
||||
|
@ -36,32 +57,87 @@ pub async fn search(
|
|||
}
|
||||
|
||||
#[get("/latest.json")]
|
||||
pub async fn latest_api(library: &State<Library>) -> FallibleApiResponse {
|
||||
pub async fn latest_api(
|
||||
library: &State<Library>,
|
||||
user: MaybeUser,
|
||||
conf: &State<Config>,
|
||||
shell: &State<Shell>,
|
||||
ctx: RequestContext,
|
||||
) -> FallibleApiResponse {
|
||||
// todo : check_private!(conf, user, shell, ctx);
|
||||
|
||||
let videos = library.get_newly_added(20).await;
|
||||
let vid_api = vec_to_api(&videos).await;
|
||||
return Ok(serde_json::json!(vid_api));
|
||||
}
|
||||
|
||||
#[get("/latest")]
|
||||
pub async fn latest_page(
|
||||
ctx: RequestContext,
|
||||
library: &State<Library>,
|
||||
user: MaybeUser,
|
||||
) -> StringResponse {
|
||||
let videos = library.get_newly_added(20).await;
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn VideoList(videos: &[Video]) -> PreEscaped<String> {
|
||||
let video_elements = html! {
|
||||
@for mut vid in videos {
|
||||
( video_element_wide(&mut vid).await );
|
||||
};
|
||||
};
|
||||
|
||||
Padding(video_elements).all(ScreenValue::_6).render()
|
||||
}
|
||||
|
||||
#[get("/latest?<offset>")]
|
||||
pub async fn latest_page(
|
||||
ctx: RequestContext,
|
||||
library: &State<Library>,
|
||||
user: MaybeUser,
|
||||
shell: &State<Shell>,
|
||||
conf: &State<Config>,
|
||||
offset: Option<u32>,
|
||||
) -> StringResponse {
|
||||
check_private!(conf, user, shell, ctx);
|
||||
|
||||
let mut videos: Vec<_> = library
|
||||
.get_newly_added(offset.unwrap_or_default() as i64 + 20)
|
||||
.await
|
||||
.into_iter()
|
||||
.skip(offset.unwrap_or_default() as usize)
|
||||
.take(20)
|
||||
.collect();
|
||||
let has_content = !videos.is_empty();
|
||||
let video_elements = VerticalVideoGrid(&mut videos).await;
|
||||
|
||||
if ctx.is_htmx && !ctx.htmx_redirect {
|
||||
return respond_html(
|
||||
Div()
|
||||
.vanish()
|
||||
.push(video_elements)
|
||||
.push_if(has_content, || {
|
||||
Width(
|
||||
ScreenValue::fit,
|
||||
Margin(InfinityScroll(
|
||||
ColoredSpinner(Purple::_600),
|
||||
&format!("/latest?offset={}", offset.unwrap_or_default() + 20),
|
||||
))
|
||||
.x(ScreenValue::auto),
|
||||
)
|
||||
})
|
||||
.render()
|
||||
.0,
|
||||
);
|
||||
}
|
||||
|
||||
let content = Div()
|
||||
.vanish()
|
||||
.push(Title("Recent videos"))
|
||||
.push(Padding(video_elements).all(ScreenValue::_6))
|
||||
.push(video_elements)
|
||||
.push(Width(
|
||||
ScreenValue::fit,
|
||||
Margin(InfinityScroll(
|
||||
ColoredSpinner(Purple::_600),
|
||||
&format!("/latest?offset={}", offset.unwrap_or_default() + 20),
|
||||
))
|
||||
.x(ScreenValue::auto),
|
||||
))
|
||||
.render();
|
||||
|
||||
render_page(ctx, content, "Recent videos", user.into()).await
|
||||
render_page(&shell, ctx, content, "Recent videos", user.into()).await
|
||||
}
|
||||
|
||||
#[get("/d/<dir>")]
|
||||
|
@ -70,7 +146,11 @@ pub async fn dir_page(
|
|||
dir: &str,
|
||||
library: &State<Library>,
|
||||
user: MaybeUser,
|
||||
shell: &State<Shell>,
|
||||
conf: &State<Config>,
|
||||
) -> StringResponse {
|
||||
check_private!(conf, user, shell, ctx);
|
||||
|
||||
if dir.ends_with(".json") {
|
||||
let dir_videos = library
|
||||
.get_directory_videos(dir.split_once(".json").map(|x| x.0).unwrap_or_default())
|
||||
|
@ -91,7 +171,11 @@ pub async fn dir_page(
|
|||
.push(Padding(video_elements).all(ScreenValue::_6))
|
||||
.render();
|
||||
|
||||
render_page(ctx, content, dir, user.into()).await
|
||||
render_page(&shell, ctx, content, dir, user.into()).await
|
||||
}
|
||||
|
||||
pub async fn is_private_page(shell: &Shell, ctx: RequestContext) -> StringResponse {
|
||||
page!(shell, ctx, "WatchDogs", Text("This is a private instance"))
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
|
@ -99,7 +183,11 @@ pub async fn index_page(
|
|||
ctx: RequestContext,
|
||||
library: &State<Library>,
|
||||
user: MaybeUser,
|
||||
shell: &State<Shell>,
|
||||
conf: &State<Config>,
|
||||
) -> StringResponse {
|
||||
check_private!(conf, user, shell, ctx);
|
||||
|
||||
let random_video_elements = html! {
|
||||
@for mut vid in library.get_random_videos(3).await {
|
||||
( video_element(&mut vid).await );
|
||||
|
@ -115,34 +203,22 @@ pub async fn index_page(
|
|||
let content = Div()
|
||||
.vanish()
|
||||
.push(Title("Random Videos"))
|
||||
.push(VerticalVideoGrid(random_video_elements))
|
||||
.push(VerticalVideoGridRaw(random_video_elements))
|
||||
.push(Title(Link("/latest", "Latest Videos").use_htmx()))
|
||||
.push(VerticalVideoGrid(newly_added_elements))
|
||||
.push(VerticalVideoGridRaw(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)
|
||||
}))
|
||||
Flex(
|
||||
Div()
|
||||
.vanish()
|
||||
.push_for_each(&directories, |dir: &_| DirectoryBadge(dir)),
|
||||
)
|
||||
.wrap(Wrap::Wrap),
|
||||
)
|
||||
.all(ScreenValue::_10),
|
||||
)
|
||||
.render();
|
||||
|
||||
render_page(ctx, content, "WatchDogs", user.into()).await
|
||||
render_page(&shell, ctx, content, "WatchDogs", user.into()).await
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use based::auth::{Sessions, User};
|
||||
use based::page;
|
||||
use based::request::StringResponse;
|
||||
use based::ui::components::{Card, Shell};
|
||||
use based::ui::prelude::*;
|
||||
use based::{auth::MaybeUser, request::RequestContext};
|
||||
use maud::{html, Render};
|
||||
use rocket::http::CookieJar;
|
||||
use rocket::State;
|
||||
use rocket::{form::Form, get, http::Cookie, post, response::Redirect, FromForm};
|
||||
|
||||
use crate::library::history::VideoHistory;
|
||||
|
@ -12,17 +15,32 @@ use crate::pages::components::{video_element_wide, Title};
|
|||
use super::components::render_page;
|
||||
|
||||
#[get("/login")]
|
||||
pub async fn login(ctx: RequestContext, user: MaybeUser) -> StringResponse {
|
||||
let content = html!(
|
||||
h2 { "Login" };
|
||||
form action="/login" method="POST" {
|
||||
input type="text" name="username" placeholder="Username" required;
|
||||
input type="password" name="password" placeholder="Password" required;
|
||||
input type="submit" value="Login";
|
||||
}
|
||||
);
|
||||
|
||||
render_page(ctx, content, "Login", user.into()).await
|
||||
pub async fn login(ctx: RequestContext, user: MaybeUser, shell: &State<Shell>) -> StringResponse {
|
||||
page!(
|
||||
shell,
|
||||
ctx,
|
||||
"Login",
|
||||
Margin(Width(
|
||||
ScreenValue::_80,
|
||||
Div().push(Text("Login")._2xl()).push(
|
||||
Margin(
|
||||
based::ui::primitives::input::Form::new("/login")
|
||||
.method(FormMethod::POST)
|
||||
.add_input(TextInput("username").placeholder("Username").required())
|
||||
.add_input(
|
||||
TextInput("password")
|
||||
.placeholder("Password")
|
||||
.required()
|
||||
.password()
|
||||
)
|
||||
.add_input(FormSubmitButton("Login").value("Login"))
|
||||
)
|
||||
.top(ScreenValue::_4)
|
||||
)
|
||||
))
|
||||
.x(ScreenValue::auto)
|
||||
.top(ScreenValue::_6)
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
|
@ -49,7 +67,7 @@ pub async fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) ->
|
|||
}
|
||||
|
||||
#[get("/history")]
|
||||
pub async fn history_page(ctx: RequestContext, user: User) -> StringResponse {
|
||||
pub async fn history_page(ctx: RequestContext, user: User, shell: &State<Shell>) -> StringResponse {
|
||||
let video_elements = html! {
|
||||
@for mut vid in user.history_of(10).await {
|
||||
( video_element_wide(&mut vid).await );
|
||||
|
@ -62,5 +80,5 @@ pub async fn history_page(ctx: RequestContext, user: User) -> StringResponse {
|
|||
.push(Padding(video_elements).all(ScreenValue::_6))
|
||||
.render();
|
||||
|
||||
render_page(ctx, content, "History", Some(user)).await
|
||||
render_page(&shell, ctx, content, "History", Some(user)).await
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use based::ui::components::Shell;
|
||||
use based::ui::primitives::space::Fraction;
|
||||
use based::ui::{prelude::*, AttrExtendable};
|
||||
use based::{
|
||||
|
@ -8,6 +9,8 @@ use based::{
|
|||
use maud::{html, PreEscaped, Render};
|
||||
use rocket::{get, State};
|
||||
|
||||
use crate::check_private;
|
||||
use crate::config::Config;
|
||||
use crate::library::Video;
|
||||
use crate::{
|
||||
library::{history::VideoHistory, Library},
|
||||
|
@ -22,7 +25,11 @@ pub async fn watch_page(
|
|||
library: &State<Library>,
|
||||
v: String,
|
||||
user: MaybeUser,
|
||||
conf: &State<Config>,
|
||||
shell: &State<Shell>,
|
||||
) -> StringResponse {
|
||||
check_private!(conf, user, shell, ctx);
|
||||
|
||||
let video = if let Some(video) = library.get_video_by_id(&v).await {
|
||||
video
|
||||
} else {
|
||||
|
@ -37,7 +44,7 @@ pub async fn watch_page(
|
|||
let youtube_meta = video.youtube_meta().await;
|
||||
let rec = build_rec(&library, &video).await;
|
||||
|
||||
let content = Container(
|
||||
let content =
|
||||
Margin(
|
||||
Screen::large(Flex(Nothing()).direction(Direction::Row)).on(
|
||||
Flex(
|
||||
|
@ -50,7 +57,7 @@ pub async fn watch_page(
|
|||
Rounded(
|
||||
Video().controls().autoplay().width(1080).add_src(
|
||||
Source(&format!("/video/raw?v={}", video.id), Some("video/mp4".to_string()))
|
||||
)
|
||||
).poster(&format!("/video/thumbnail?v={}", video.id))
|
||||
).size(Size::Large)
|
||||
).color(Colors::Black)
|
||||
))
|
||||
|
@ -94,10 +101,10 @@ pub async fn watch_page(
|
|||
rec
|
||||
)
|
||||
).direction(Direction::Column).gap(ScreenValue::_6))
|
||||
).x(ScreenValue::_10).top(ScreenValue::_6)
|
||||
).render();
|
||||
).x(ScreenValue::_10).top(ScreenValue::_6).render();
|
||||
|
||||
render_page(
|
||||
&shell,
|
||||
ctx,
|
||||
content,
|
||||
&format!("{} - WatchDogs", video.title),
|
||||
|
@ -123,10 +130,18 @@ pub async fn build_rec(library: &Library, video: &Video) -> PreEscaped<String> {
|
|||
Margin(
|
||||
Paragraph(Context(
|
||||
SpaceBetween(
|
||||
Flex(Div().vanish().push(Span("In ")).push(Link(
|
||||
&format!("/d/{}", video.directory),
|
||||
Text(&video.directory).color(&Blue::_500),
|
||||
)))
|
||||
Flex(
|
||||
Div()
|
||||
.vanish()
|
||||
.push(Span("In ")._4xl().extrabold())
|
||||
.push(Link(
|
||||
&format!("/d/{}", video.directory),
|
||||
Text(&video.directory)
|
||||
.color(&Blue::_500)
|
||||
._4xl()
|
||||
.extrabold(),
|
||||
)),
|
||||
)
|
||||
.group()
|
||||
.justify(Justify::Center),
|
||||
)
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
function stopAllVideos() {
|
||||
const videos = document.querySelectorAll('video');
|
||||
|
||||
videos.forEach(video => {
|
||||
video.pause();
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue