Support enabling/disabling download buttons for videos (disabled by default)

This commit is contained in:
Simon Repp
2025-02-20 21:26:42 +01:00
parent 36cd41f76b
commit c3e9c45e77
14 changed files with 286 additions and 11 deletions
+21 -1
View File
@@ -37,12 +37,13 @@ Content in a collection currently appears in the following order on the built si
## The collection.eno manifest
> All options at a glance: [description](#description), [embedding](#embedding), [link](#link), [offline](#offline), [title](#title), [unlisted](#unlisted), [video_order](#video_order)
> All options at a glance: [description](#description), [download](#download), [embedding](#embedding), [link](#link), [offline](#offline), [title](#title), [unlisted](#unlisted), [video_order](#video_order)
Manifest for tagging and configuring a collection.
```eno
title: Soothing clips
download: enabled
offline
video_order: sort_number_desc
unlisted
@@ -70,6 +71,25 @@ additionally use markdown-style inline links such as `[Example](https://example.
the text of a link (Note that no other markdown syntax is supported inside `description` though).
Any number of empty lines in the description field separates paragraphs in the text.
## <a name="download"></a> `download`
This opt-in setting enables download buttons on video pages, openly inviting
and allowing visitors to conveniently choose and download a specific version
of a video to their computer.
```eno
download: enabled
```
Note that the absence of a download button (i.e. the default setting of this
option) does not mean that it is not possible to download your videos - this
is always easily possible for anyone with some technical knowledge. Enabling
download buttons however is an invitation to your visitors that you want them
to be easily able to do so, and inversely, not enabling it - as on every
other website - simply implies that you do not openly invite downloads, and
visitors will have to research or inquire about your video's license before
making any use of it beyond watching it on your site.
## <a name="embedding"></a> `embedding`
This allows visitors/you to copy embed codes (html) that they/you can use to
+21 -1
View File
@@ -30,7 +30,7 @@ sequential playback.
## The playlist.eno manifest
> All options at a glance: [banner](#banner), [description](#description), [embedding](#embedding), [link](#link), [offline](#offline), [order](#order), [title](#title), [unlisted](#unlisted)
> All options at a glance: [banner](#banner), [description](#description), [download](#download), [embedding](#embedding), [link](#link), [offline](#offline), [order](#order), [title](#title), [unlisted](#unlisted)
Manifest for tagging and configuring a playlist.
@@ -39,6 +39,7 @@ banner:
description = Colorful flags in the wind
file = my_poster.jpg
download: enabled
embedding: enabled
offline
order: release_date_asc
@@ -78,6 +79,25 @@ additionally use markdown-style inline links such as `[Example](https://example.
the text of a link (Note that no other markdown syntax is supported inside `description` though).
Any number of empty lines in the description field separates paragraphs in the text.
## <a name="download"></a> `download`
This opt-in setting enables download buttons on video pages, openly inviting
and allowing visitors to conveniently choose and download a specific version
of a video to their computer.
```eno
download: enabled
```
Note that the absence of a download button (i.e. the default setting of this
option) does not mean that it is not possible to download your videos - this
is always easily possible for anyone with some technical knowledge. Enabling
download buttons however is an invitation to your visitors that you want them
to be easily able to do so, and inversely, not enabling it - as on every
other website - simply implies that you do not openly invite downloads, and
visitors will have to research or inquire about your video's license before
making any use of it beyond watching it on your site.
## <a name="embedding"></a> `embedding`
This allows visitors/you to copy embed codes (html) that they/you can use to
+21 -1
View File
@@ -39,7 +39,7 @@ subtitles while `fr.vtt` would indicate french subtitles.
## The video.eno manifest
> All options at a glance: [description](#description), [embedding](#embedding), [link](#link), [offline](#offline), [poster](#poster), [release_date](#release_date), [sort_number](#sort_number), [title](#title), [unlisted](#unlisted)
> All options at a glance: [description](#description), [download](#download), [embedding](#embedding), [link](#link), [offline](#offline), [poster](#poster), [release_date](#release_date), [sort_number](#sort_number), [title](#title), [unlisted](#unlisted)
Manifest for tagging and configuring a video, which can be placed inside the
video directory. A video directory can be identified as a video directory
@@ -51,6 +51,7 @@ poster:
description = Confused looking people in an office
file = episode_1.jpg
download: enabled
offline
release_date: 2024-02-12
sort_number: 1
@@ -79,6 +80,25 @@ additionally use markdown-style inline links such as `[Example](https://example.
the text of a link (Note that no other markdown syntax is supported inside `description` though).
Any number of empty lines in the description field separates paragraphs in the text.
## <a name="download"></a> `download`
This opt-in setting enables a download button on the video page, openly inviting
and allowing visitors to conveniently choose and download a specific version
of the video to their computer.
```eno
download: enabled
```
Note that the absence of a download button (i.e. the default setting of this
option) does not mean that it is not possible to download your video - this
is always easily possible for anyone with some technical knowledge. Enabling
the download button however is an invitation to your visitors that you want them
to be easily able to do so, and inversely, not enabling it - as on every
other website - simply implies that you do not openly invite downloading, and
visitors will have to research or inquire about your video's license before
making any use of it beyond watching it on your site.
## <a name="embedding"></a> `embedding`
This allows visitors/you to copy embed codes (html) that they/you can use to
+12 -4
View File
@@ -35,16 +35,17 @@ use video::video_html;
/// Some options need to be evaluated in the context of the entire site tree
/// before they are applied at the individual node level. This includes
/// embedding, where the option is inherited down the tree but potentially
/// overriden at the node level, as well as the unlisted option, which
/// unconditionally applies to all descendants of a site tree branch whose
/// (in)direct parent node is marked as unlisted.
/// embedding and download, where the option is inherited down the tree but
/// potentially overriden at the node level, as well as the unlisted option,
/// which unconditionally applies to all descendants of a site tree branch
/// whose(in)direct parent node is marked as unlisted.
///
/// MergeOptions represents these options, and at the node level we only
/// consider the options on this struct (after we have merged in the options
/// from the node level beforehand of course).
#[derive(Clone)]
struct MergedOptions {
pub download: bool,
pub embedding: bool,
pub unlisted: bool
}
@@ -52,36 +53,43 @@ struct MergedOptions {
impl MergedOptions {
pub fn default() -> MergedOptions {
MergedOptions {
download: false,
embedding: false,
unlisted: false
}
}
pub fn merge_collection_options(&self, collection: &Collection) -> MergedOptions {
let download = collection.download.unwrap_or(self.download);
let embedding = collection.embedding.unwrap_or(self.embedding);
let unlisted = self.unlisted || collection.unlisted;
MergedOptions {
download,
embedding,
unlisted
}
}
pub fn merge_playlist_options(&self, playlist: &Playlist) -> MergedOptions {
let download = playlist.download.unwrap_or(self.download);
let embedding = playlist.embedding.unwrap_or(self.embedding);
let unlisted = self.unlisted || playlist.unlisted;
MergedOptions {
download,
embedding,
unlisted
}
}
pub fn merge_video_options(&self, video: &Video) -> MergedOptions {
let download = video.download.unwrap_or(self.download);
let embedding = video.embedding.unwrap_or(self.embedding);
let unlisted = self.unlisted || video.unlisted;
MergedOptions {
download,
embedding,
unlisted
}
+4 -1
View File
@@ -38,7 +38,10 @@ pub fn video_html(
};
let e_title = html_escape_outside_attribute(title);
let r_downloads = downloads(site, video);
let r_downloads = match merged_options.download {
true => downloads(site, video),
false => String::new()
};
let next_video = if video.unlisted {
None
+34
View File
@@ -32,6 +32,7 @@ use crate::util::checked_remove_dir_all;
const COLLECTION_OPTIONS: &[&str] = &[
"banner",
"description",
"download",
"embedding",
"link",
"offline",
@@ -44,6 +45,7 @@ const COLLECTION_OPTIONS: &[&str] = &[
pub struct Collection {
pub banner: Option<Banner>,
pub description: Option<String>,
pub download: Option<bool>,
pub embedding: Option<bool>,
pub errors: Vec<String>,
pub links: Vec<Link>,
@@ -141,6 +143,33 @@ impl Collection {
self.errors.push(error);
}
}
"download" => 'download: {
if let Ok(field) = element.as_field() {
if let Ok(result) = field.value() {
if let Some(value) = result {
match value {
"enabled" => {
self.download = Some(true);
}
"disabled" => {
self.download = Some(false);
}
other => {
let message = format!("The download option only supports the value 'enabled' or 'disabled'. The given value '{other}' was not recognized.");
let error = element_error(element, manifest_path, &message);
self.errors.push(error);
}
}
break 'download;
}
}
}
let message = "download needs to be provided as a field with the value 'enabled' or 'disabled', e.g.: 'download: enabled'";
let error = element_error(element, manifest_path, message);
self.errors.push(error);
}
"embedding" => 'embedding: {
if let Ok(field) = element.as_field() {
if let Ok(result) = field.value() {
@@ -598,6 +627,7 @@ impl Collection {
Collection {
banner: None,
description: None,
download: None,
embedding: None,
errors: Vec::new(),
links: Vec::new(),
@@ -1003,6 +1033,10 @@ impl Collection {
eno.push_str(&format!("file = {}\n", banner.file_name));
}
if let Some(download) = self.download {
eno.push_str(&format!("download: {}\n", if download { "enabled" } else { "disabled" }));
}
if let Some(embedding) = self.embedding {
eno.push_str(&format!("embedding: {}\n", if embedding { "enabled" } else { "disabled" }));
}
+26 -1
View File
@@ -65,6 +65,7 @@ pub enum CollectionFormFeedback<'a> {
Site(endpoints::site::SiteFormFeedback),
Update {
description: &'a str,
download: Option<bool>,
embedding: Option<bool>,
offline: bool,
title: Field,
@@ -145,10 +146,16 @@ pub fn edit_page(
let mut input_offline = Checkbox::new("offline", translations.offline);
let mut input_title = Input::text("title", translations.title);
let mut input_unlisted = Checkbox::new("unlisted", translations.unlisted);
let mut select_download = Select::new("download", translations.download);
let mut select_embedding = Select::new("embedding", translations.embedding);
let mut select_video_order = Select::new("video_order", translations.video_order);
let mut textarea_description = Textarea::markdown_subset(language, "description", translations.description);
// TODO: Dynamically determine and additionally display what the default or inherited value is here.
select_download.option("", translations.inherit);
select_download.option("enabled", translations.enabled);
select_download.option("disabled", translations.disabled);
// TODO: Dynamically determine and additionally display what the default or inherited value is here.
select_embedding.option("", translations.inherit);
select_embedding.option("enabled", translations.enabled);
@@ -167,12 +174,19 @@ pub fn edit_page(
let feedback_update = if let CollectionFormFeedback::Update {
description,
download,
embedding,
offline,
title,
unlisted,
video_order
} = &form_feedback {
let download_value = match download {
Some(true) => "enabled",
Some(false) => "disabled",
None => ""
};
let embedding_value = match embedding {
Some(true) => "enabled",
Some(false) => "disabled",
@@ -182,12 +196,19 @@ pub fn edit_page(
input_offline.checked(*offline);
input_title.validated_value(title);
input_unlisted.checked(*unlisted);
select_download.selected(download_value);
select_embedding.selected(embedding_value);
select_video_order.selected(video_order);
textarea_description.value(description);
format!(r#"<span class="feedback">{t_not_saved}</span>"#)
} else {
let download = match collection.download {
Some(true) => "enabled",
Some(false) => "disabled",
None => ""
};
let embedding = match collection.embedding {
Some(true) => "enabled",
Some(false) => "disabled",
@@ -197,6 +218,7 @@ pub fn edit_page(
input_offline.checked(collection.offline);
input_title.value_option(&collection.title);
input_unlisted.checked(collection.unlisted);
select_download.selected(download);
select_embedding.selected(embedding);
select_video_order.selected(collection.video_order.key());
textarea_description.value_option(&collection.description);
@@ -432,7 +454,10 @@ pub fn edit_page(
{input_title}
{textarea_description}
{select_video_order}
{select_embedding}
<div class="form_split even">
{select_download}
{select_embedding}
</div>
{input_offline}
{input_unlisted_conditionally_hidden}
@@ -16,6 +16,7 @@ use super::CollectionFormFeedback;
#[derive(Deserialize)]
pub struct UpdateForm {
description: String,
download: String,
embedding: String,
offline: Option<String>,
title: String,
@@ -32,6 +33,12 @@ pub async fn update(
let description_trimmed = form.description.trim();
let download = match form.download.as_str() {
"disabled" => Some(false),
"enabled" => Some(true),
_ => None
};
let embedding = match form.embedding.as_str() {
"disabled" => Some(false),
"enabled" => Some(true),
@@ -47,6 +54,7 @@ pub async fn update(
if true {
let mutation = |collection_mut: &mut Collection| {
collection_mut.description = if description_trimmed.is_empty() { None } else { Some(description_trimmed.to_owned()) };
collection_mut.download = download;
collection_mut.embedding = embedding;
collection_mut.offline = offline;
collection_mut.title = if title_trimmed.is_empty() { None } else { Some(title_trimmed.to_owned()) };
@@ -72,6 +80,7 @@ pub async fn update(
let form_feedback = CollectionFormFeedback::Update {
description: description_trimmed,
download,
embedding,
offline,
title: Field::valid(title_trimmed),
+26 -1
View File
@@ -53,6 +53,7 @@ pub enum PlaylistFormFeedback<'a> {
Site(endpoints::site::SiteFormFeedback),
Update {
description: &'a str,
download: Option<bool>,
embedding: Option<bool>,
offline: bool,
order: &'a str,
@@ -135,10 +136,16 @@ pub fn edit_page(
let mut input_offline = Checkbox::new("offline", translations.offline);
let mut input_title = Input::text("title", translations.title);
let mut input_unlisted = Checkbox::new("unlisted", translations.unlisted);
let mut select_download = Select::new("download", translations.download);
let mut select_embedding = Select::new("embedding", translations.embedding);
let mut select_order = Select::new("order", translations.order);
let mut textarea_description = Textarea::markdown_subset(language, "description", translations.description);
// TODO: Dynamically determine and additionally display what the default or inherited value is here.
select_download.option("", translations.inherit);
select_download.option("enabled", translations.enabled);
select_download.option("disabled", translations.disabled);
// TODO: Dynamically determine and additionally display what the default or inherited value is here.
select_embedding.option("", translations.inherit);
select_embedding.option("enabled", translations.enabled);
@@ -156,12 +163,19 @@ pub fn edit_page(
let feedback_update = if let Some(PlaylistFormFeedback::Update {
description,
download,
embedding,
offline,
order,
title,
unlisted
}) = &form_feedback {
let download_value = match download {
Some(true) => "enabled",
Some(false) => "disabled",
None => ""
};
let embedding_value = match embedding {
Some(true) => "enabled",
Some(false) => "disabled",
@@ -171,12 +185,19 @@ pub fn edit_page(
input_offline.checked(*offline);
input_title.validated_value(title);
input_unlisted.checked(*unlisted);
select_download.selected(download_value);
select_embedding.selected(embedding_value);
select_order.selected(order);
textarea_description.value(description);
format!(r#"<span class="feedback">{t_not_saved}</span>"#)
} else {
let download = match playlist.download {
Some(true) => "enabled",
Some(false) => "disabled",
None => ""
};
let embedding = match playlist.embedding {
Some(true) => "enabled",
Some(false) => "disabled",
@@ -186,6 +207,7 @@ pub fn edit_page(
input_offline.checked(playlist.offline);
input_title.value_option(&playlist.title);
input_unlisted.checked(playlist.unlisted);
select_download.selected(download);
select_embedding.selected(embedding);
select_order.selected(playlist.order.key());
textarea_description.value_option(&playlist.description);
@@ -338,7 +360,10 @@ pub fn edit_page(
{input_title}
{textarea_description}
{select_order}
{select_embedding}
<div class="form_split even">
{select_download}
{select_embedding}
</div>
{input_offline}
{input_unlisted_conditionally_hidden}
+9
View File
@@ -16,6 +16,7 @@ use super::PlaylistFormFeedback;
#[derive(Deserialize)]
pub struct PlaylistUpdateForm {
description: String,
download: String,
embedding: String,
offline: Option<String>,
order: String,
@@ -32,6 +33,12 @@ pub async fn update(
let description_trimmed = form.description.trim();
let download = match form.download.as_str() {
"disabled" => Some(false),
"enabled" => Some(true),
_ => None
};
let embedding = match form.embedding.as_str() {
"disabled" => Some(false),
"enabled" => Some(true),
@@ -47,6 +54,7 @@ pub async fn update(
if true {
let mutation = |playlist_mut: &mut Playlist| {
playlist_mut.description = if description_trimmed.is_empty() { None } else { Some(description_trimmed.to_owned()) };
playlist_mut.download = download;
playlist_mut.embedding = embedding;
playlist_mut.offline = offline;
playlist_mut.order = order;
@@ -70,6 +78,7 @@ pub async fn update(
let form_feedback = PlaylistFormFeedback::Update {
description: description_trimmed,
download,
embedding,
offline,
order: &form.order,
+26 -1
View File
@@ -63,6 +63,7 @@ pub enum VideoFormFeedback<'a> {
Site(endpoints::site::SiteFormFeedback),
Update {
description: &'a str,
download: Option<bool>,
embedding: Option<bool>,
offline: bool,
release_date: Field,
@@ -417,9 +418,15 @@ pub fn edit_page(
let mut input_sort_number = Input::number("sort_number", translations.sort_number);
let mut input_title = Input::text("title", translations.title);
let mut input_unlisted = Checkbox::new("unlisted", translations.unlisted);
let mut select_download = Select::new("download", translations.download);
let mut select_embedding = Select::new("embedding", translations.embedding);
let mut textarea_description = Textarea::markdown_subset(language, "description", translations.description);
// TODO: Dynamically determine and additionally display what the default or inherited value is here.
select_download.option("", translations.inherit);
select_download.option("enabled", translations.enabled);
select_download.option("disabled", translations.disabled);
// TODO: Dynamically determine and additionally display what the default or inherited value is here.
select_embedding.option("", translations.inherit);
select_embedding.option("enabled", translations.enabled);
@@ -429,6 +436,7 @@ pub fn edit_page(
let feedback_update = if let Some(VideoFormFeedback::Update {
description,
download,
embedding,
offline,
release_date,
@@ -436,6 +444,12 @@ pub fn edit_page(
title,
unlisted
}) = &form_feedback {
let download_value = match download {
Some(true) => "enabled",
Some(false) => "disabled",
None => ""
};
let embedding_value = match embedding {
Some(true) => "enabled",
Some(false) => "disabled",
@@ -447,11 +461,18 @@ pub fn edit_page(
input_sort_number.validated_value(sort_number);
input_title.validated_value(title);
input_unlisted.checked(*unlisted);
select_download.selected(download_value);
select_embedding.selected(embedding_value);
textarea_description.value(description);
format!(r#"<span class="feedback">{t_not_saved}</span>"#)
} else {
let download = match video.download {
Some(true) => "enabled",
Some(false) => "disabled",
None => ""
};
let embedding = match video.embedding {
Some(true) => "enabled",
Some(false) => "disabled",
@@ -465,6 +486,7 @@ pub fn edit_page(
input_sort_number.value_option(&sort_number);
input_title.value_option(&video.title);
input_unlisted.checked(video.unlisted);
select_download.selected(download);
select_embedding.selected(embedding);
textarea_description.value_option(&video.description);
@@ -573,7 +595,10 @@ pub fn edit_page(
{input_release_date}
{input_sort_number}
</div>
{select_embedding}
<div class="form_split even">
{select_download}
{select_embedding}
</div>
{input_offline}
{input_unlisted_conditionally_hidden}
+9
View File
@@ -17,6 +17,7 @@ use super::VideoFormFeedback;
#[derive(Deserialize)]
pub struct VideoUpdateForm {
description: String,
download: String,
embedding: String,
offline: Option<String>,
release_date: String,
@@ -34,6 +35,12 @@ pub async fn update(
let description_trimmed = form.description.trim();
let download = match form.download.as_str() {
"disabled" => Some(false),
"enabled" => Some(true),
_ => None
};
let embedding = match form.embedding.as_str() {
"disabled" => Some(false),
"enabled" => Some(true),
@@ -74,6 +81,7 @@ pub async fn update(
let form_feedback = VideoFormFeedback::Update {
description: description_trimmed,
download,
embedding,
offline,
release_date: Field::validated(release_date_trimmed, release_date.and_then(|release_date| release_date.err())),
@@ -88,6 +96,7 @@ pub async fn update(
} else {
let mutation = |video_mut: &mut Video| {
video_mut.description = if description_trimmed.is_empty() { None } else { Some(description_trimmed.to_owned()) };
video_mut.download = download;
video_mut.embedding = embedding;
video_mut.offline = offline;
video_mut.release_date = release_date.and_then(|release_date| release_date.ok());
+34
View File
@@ -30,6 +30,7 @@ use crate::util::checked_remove_dir_all;
const PLAYLIST_OPTIONS: &[&str] = &[
"banner",
"description",
"download",
"embedding",
"link",
"offline",
@@ -42,6 +43,7 @@ const PLAYLIST_OPTIONS: &[&str] = &[
pub struct Playlist {
pub banner: Option<Banner>,
pub description: Option<String>,
pub download: Option<bool>,
pub embedding: Option<bool>,
pub errors: Vec<String>,
pub links: Vec<Link>,
@@ -146,6 +148,33 @@ impl Playlist {
self.errors.push(error);
}
}
"download" => 'download: {
if let Ok(field) = element.as_field() {
if let Ok(result) = field.value() {
if let Some(value) = result {
match value {
"enabled" => {
self.download = Some(true);
}
"disabled" => {
self.download = Some(false);
}
other => {
let message = format!("The download option only supports the value 'enabled' or 'disabled'. The given value '{other}' was not recognized.");
let error = element_error(element, manifest_path, &message);
self.errors.push(error);
}
}
break 'download;
}
}
}
let message = "download needs to be provided as a field with the value 'enabled' or 'disabled', e.g.: 'download: enabled'";
let error = element_error(element, manifest_path, message);
self.errors.push(error);
}
"embedding" => 'embedding: {
if let Ok(field) = element.as_field() {
if let Ok(result) = field.value() {
@@ -418,6 +447,7 @@ impl Playlist {
Playlist {
banner: None,
description: None,
download: None,
embedding: None,
errors: Vec::new(),
links: Vec::new(),
@@ -630,6 +660,10 @@ impl Playlist {
eno.push_str(&format!("file = {}\n", banner.file_name));
}
if let Some(download) = self.download {
eno.push_str(&format!("download: {}\n", if download { "enabled" } else { "disabled" }));
}
if let Some(embedding) = self.embedding {
eno.push_str(&format!("embedding: {}\n", if embedding { "enabled" } else { "disabled" }));
}
+34
View File
@@ -29,6 +29,7 @@ use crate::util::{checked_remove_dir_all, hash};
const VIDEO_OPTIONS: &[&str] = &[
"description",
"download",
"embedding",
"link",
"offline",
@@ -42,6 +43,7 @@ const VIDEO_OPTIONS: &[&str] = &[
#[derive(Clone, Debug)]
pub struct Video {
pub description: Option<String>,
pub download: Option<bool>,
pub embedding: Option<bool>,
pub errors: Vec<String>,
pub links: Vec<Link>,
@@ -122,6 +124,33 @@ impl Video {
self.errors.push(error);
}
}
"download" => 'download: {
if let Ok(field) = element.as_field() {
if let Ok(result) = field.value() {
if let Some(value) = result {
match value {
"enabled" => {
self.download = Some(true);
}
"disabled" => {
self.download = Some(false);
}
other => {
let message = format!("The download option only supports the value 'enabled' or 'disabled'. The given value '{other}' was not recognized.");
let error = element_error(element, manifest_path, &message);
self.errors.push(error);
}
}
break 'download;
}
}
}
let message = "download needs to be provided as a field with the value 'enabled' or 'disabled', e.g.: 'download: enabled'";
let error = element_error(element, manifest_path, message);
self.errors.push(error);
}
"embedding" => 'embedding: {
if let Ok(field) = element.as_field() {
if let Ok(result) = field.value() {
@@ -570,6 +599,7 @@ impl Video {
pub fn new(path: String) -> Video {
Video {
description: None,
download: None,
embedding: None,
errors: Vec::new(),
links: Vec::new(),
@@ -725,6 +755,10 @@ impl Video {
eno.push_str(&link.to_eno());
}
if let Some(download) = self.download {
eno.push_str(&format!("download: {}\n", if download { "enabled" } else { "disabled" }));
}
if let Some(embedding) = self.embedding {
eno.push_str(&format!("embedding: {}\n", if embedding { "enabled" } else { "disabled" }));
}