diff --git a/docs/reference/01 Collection.md b/docs/reference/01 Collection.md
index 87c2ac3..74515c9 100644
--- a/docs/reference/01 Collection.md
+++ b/docs/reference/01 Collection.md
@@ -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.
+## `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.
+
## `embedding`
This allows visitors/you to copy embed codes (html) that they/you can use to
diff --git a/docs/reference/02 Playlist.md b/docs/reference/02 Playlist.md
index d8d02bf..ce38c2c 100644
--- a/docs/reference/02 Playlist.md
+++ b/docs/reference/02 Playlist.md
@@ -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.
+## `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.
+
## `embedding`
This allows visitors/you to copy embed codes (html) that they/you can use to
diff --git a/docs/reference/03 Video.md b/docs/reference/03 Video.md
index f67a2b7..c9a36a9 100644
--- a/docs/reference/03 Video.md
+++ b/docs/reference/03 Video.md
@@ -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.
+## `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.
+
## `embedding`
This allows visitors/you to copy embed codes (html) that they/you can use to
diff --git a/src/build.rs b/src/build.rs
index a10fb07..b791491 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -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
}
diff --git a/src/build/video.rs b/src/build/video.rs
index b80016c..751979d 100644
--- a/src/build/video.rs
+++ b/src/build/video.rs
@@ -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
diff --git a/src/collection.rs b/src/collection.rs
index a159023..5144349 100644
--- a/src/collection.rs
+++ b/src/collection.rs
@@ -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,
pub description: Option,
+ pub download: Option,
pub embedding: Option,
pub errors: Vec,
pub links: Vec,
@@ -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" }));
}
diff --git a/src/editor/endpoints/collection.rs b/src/editor/endpoints/collection.rs
index 693d396..73b315f 100644
--- a/src/editor/endpoints/collection.rs
+++ b/src/editor/endpoints/collection.rs
@@ -65,6 +65,7 @@ pub enum CollectionFormFeedback<'a> {
Site(endpoints::site::SiteFormFeedback),
Update {
description: &'a str,
+ download: Option,
embedding: Option,
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#"{t_not_saved}"#)
} 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}
+
+ {select_download}
+ {select_embedding}
+
{input_offline}
{input_unlisted_conditionally_hidden}
diff --git a/src/editor/endpoints/collection/update.rs b/src/editor/endpoints/collection/update.rs
index 2736698..cfed468 100644
--- a/src/editor/endpoints/collection/update.rs
+++ b/src/editor/endpoints/collection/update.rs
@@ -16,6 +16,7 @@ use super::CollectionFormFeedback;
#[derive(Deserialize)]
pub struct UpdateForm {
description: String,
+ download: String,
embedding: String,
offline: Option,
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),
diff --git a/src/editor/endpoints/playlist.rs b/src/editor/endpoints/playlist.rs
index 4910ef1..72409e8 100644
--- a/src/editor/endpoints/playlist.rs
+++ b/src/editor/endpoints/playlist.rs
@@ -53,6 +53,7 @@ pub enum PlaylistFormFeedback<'a> {
Site(endpoints::site::SiteFormFeedback),
Update {
description: &'a str,
+ download: Option,
embedding: Option,
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#"{t_not_saved}"#)
} 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}
+