Add video download button

Fixes #263
This commit is contained in:
Zed 2026-06-21 20:37:29 +02:00
commit 279fa09771
10 changed files with 93 additions and 41 deletions

View file

@ -1,12 +1,12 @@
@font-face { @font-face {
font-family: "fontello"; font-family: "fontello";
src: url("/fonts/fontello.eot?49059696"); src: url("/fonts/fontello.eot?59696369");
src: src:
url("/fonts/fontello.eot?49059696#iefix") format("embedded-opentype"), url("/fonts/fontello.eot?59696369#iefix") format("embedded-opentype"),
url("/fonts/fontello.woff2?49059696") format("woff2"), url("/fonts/fontello.woff2?59696369") format("woff2"),
url("/fonts/fontello.woff?49059696") format("woff"), url("/fonts/fontello.woff?59696369") format("woff"),
url("/fonts/fontello.ttf?49059696") format("truetype"), url("/fonts/fontello.ttf?59696369") format("truetype"),
url("/fonts/fontello.svg?49059696#fontello") format("svg"); url("/fonts/fontello.svg?59696369#fontello") format("svg");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -36,113 +36,118 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-group:before {
content: "\e0c3";
}
/* '' */
.icon-views:before { .icon-views:before {
content: "\e800"; content: "\e800";
} }
/* '' */ /* '' */
.icon-heart:before { .icon-heart:before {
content: "\e801"; content: "\e801";
} }
/* '' */ /* '' */
.icon-quote:before { .icon-quote:before {
content: "\e802"; content: "\e802";
} }
/* '' */ /* '' */
.icon-comment:before { .icon-comment:before {
content: "\e803"; content: "\e803";
} }
/* '' */ /* '' */
.icon-group:before {
content: "\e804";
}
/* '' */
.icon-play:before { .icon-play:before {
content: "\e805"; content: "\e805";
} }
/* '' */ /* '' */
.icon-link:before { .icon-link:before {
content: "\e806"; content: "\e806";
} }
/* '' */ /* '' */
.icon-calendar:before { .icon-calendar:before {
content: "\e807"; content: "\e807";
} }
/* '' */ /* '' */
.icon-location:before { .icon-location:before {
content: "\e808"; content: "\e808";
} }
/* '' */ /* '' */
.icon-picture:before { .icon-picture:before {
content: "\e809"; content: "\e809";
} }
/* '' */ /* '' */
.icon-lock:before { .icon-lock:before {
content: "\e80a"; content: "\e80a";
} }
/* '' */ /* '' */
.icon-down:before { .icon-down:before {
content: "\e80b"; content: "\e80b";
} }
/* '' */ /* '' */
.icon-retweet:before { .icon-retweet:before {
content: "\e80c"; content: "\e80c";
} }
/* '' */ /* '' */
.icon-search:before { .icon-search:before {
content: "\e80d"; content: "\e80d";
} }
/* '' */ /* '' */
.icon-pin:before { .icon-pin:before {
content: "\e80e"; content: "\e80e";
} }
/* '' */ /* '' */
.icon-cog:before { .icon-cog:before {
content: "\e80f"; content: "\e80f";
} }
/* '' */ /* '' */
.icon-rss:before { .icon-rss:before {
content: "\e810"; content: "\e810";
} }
/* '' */ /* '' */
.icon-ok:before { .icon-ok:before {
content: "\e811"; content: "\e811";
} }
/* '' */ /* '' */
.icon-attention:before { .icon-attention-circled:before {
content: "\e812"; content: "\e812";
} }
/* '' */ /* '' */
.icon-download-alt:before {
content: "\e813";
}
/* '' */
.icon-circle:before { .icon-circle:before {
content: "\f111"; content: "\f111";
} }
/* '' */ /* '' */
.icon-info:before { .icon-info:before {
content: "\f128"; content: "\f128";
} }
/* '' */ /* '' */
.icon-bird:before { .icon-bird:before {
content: "\f309"; content: "\f309";
} }
/* '' */ /* '' */

Binary file not shown.

View file

@ -6,6 +6,8 @@
<font id="fontello" horiz-adv-x="1000" > <font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" /> <font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" /> <missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="group" unicode="&#xe0c3;" d="M0 106l0 134q0 26 18 32l171 80q-66 39-68 131 0 56 35 103 37 41 90 43 31 0 63-19-49-125 23-237-12-11-25-19l-114-55q-48-23-52-84l0-143-114 0q-25 0-27 34z m193-59l0 168q0 27 22 37l152 70 57 28q-37 23-60 66t-22 94q0 76 46 130t110 54 109-54 45-130q0-105-78-158l61-30 146-70q24-10 24-37l0-168q-2-37-37-41l-541 0q-14 2-24 14t-10 27z m473 330q68 106 22 231 31 19 66 21 49 0 90-43 35-41 35-103 0-82-65-131l168-80q18-10 18-32l0-134q0-32-27-34l-118 0 0 143q0 57-50 84l-110 53q-15 8-29 25z" horiz-adv-x="1000" />
<glyph glyph-name="views" unicode="&#xe800;" d="M180 516l0-538-180 0 0 538 180 0z m250-138l0-400-180 0 0 400 180 0z m250 344l0-744-180 0 0 744 180 0z" horiz-adv-x="680" /> <glyph glyph-name="views" unicode="&#xe800;" d="M180 516l0-538-180 0 0 538 180 0z m250-138l0-400-180 0 0 400 180 0z m250 344l0-744-180 0 0 744 180 0z" horiz-adv-x="680" />
<glyph glyph-name="heart" unicode="&#xe801;" d="M790 644q70-64 70-156t-70-158l-360-330-360 330q-70 66-70 158t70 156q62 58 151 58t153-58l56-52 58 52q62 58 150 58t152-58z" horiz-adv-x="860" /> <glyph glyph-name="heart" unicode="&#xe801;" d="M790 644q70-64 70-156t-70-158l-360-330-360 330q-70 66-70 158t70 156q62 58 151 58t153-58l56-52 58 52q62 58 150 58t152-58z" horiz-adv-x="860" />
@ -14,8 +16,6 @@
<glyph glyph-name="comment" unicode="&#xe803;" d="M1000 350q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12-10-1-17 5t-10 16v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 73 40 139t106 114 160 76 194 28q136 0 251-48t182-130 67-179z" horiz-adv-x="1000" /> <glyph glyph-name="comment" unicode="&#xe803;" d="M1000 350q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12-10-1-17 5t-10 16v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 73 40 139t106 114 160 76 194 28q136 0 251-48t182-130 67-179z" horiz-adv-x="1000" />
<glyph glyph-name="group" unicode="&#xe804;" d="M0 106l0 134q0 26 18 32l171 80q-66 39-68 131 0 56 35 103 37 41 90 43 31 0 63-19-49-125 23-237-12-11-25-19l-114-55q-48-23-52-84l0-143-114 0q-25 0-27 34z m193-59l0 168q0 27 22 37l152 70 57 28q-37 23-60 66t-22 94q0 76 46 130t110 54 109-54 45-130q0-105-78-158l61-30 146-70q24-10 24-37l0-168q-2-37-37-41l-541 0q-14 2-24 14t-10 27z m473 330q68 106 22 231 31 19 66 21 49 0 90-43 35-41 35-103 0-82-65-131l168-80q18-10 18-32l0-134q0-32-27-34l-118 0 0 143q0 57-50 84l-110 53q-15 8-29 25z" horiz-adv-x="1000" />
<glyph glyph-name="play" unicode="&#xe805;" d="M772 333l-741-412q-13-7-22-2t-9 20v822q0 14 9 20t22-2l741-412q13-7 13-17t-13-17z" horiz-adv-x="785.7" /> <glyph glyph-name="play" unicode="&#xe805;" d="M772 333l-741-412q-13-7-22-2t-9 20v822q0 14 9 20t22-2l741-412q13-7 13-17t-13-17z" horiz-adv-x="785.7" />
<glyph glyph-name="link" unicode="&#xe806;" d="M294 116q14 14 34 14t36-14q32-34 0-70l-42-40q-56-56-132-56-78 0-134 56t-56 132q0 78 56 134l148 148q70 68 144 77t128-43q16-16 16-36t-16-36q-36-32-70 0-50 48-132-34l-148-146q-26-26-26-64t26-62q26-26 63-26t63 26z m450 574q56-56 56-132 0-78-56-134l-158-158q-74-72-150-72-62 0-112 50-14 14-14 34t14 36q14 14 35 14t35-14q50-48 122 24l158 156q28 28 28 64 0 38-28 62-24 26-56 31t-60-21l-50-50q-16-14-36-14t-34 14q-34 34 0 70l50 50q54 54 127 51t129-61z" horiz-adv-x="800" /> <glyph glyph-name="link" unicode="&#xe806;" d="M294 116q14 14 34 14t36-14q32-34 0-70l-42-40q-56-56-132-56-78 0-134 56t-56 132q0 78 56 134l148 148q70 68 144 77t128-43q16-16 16-36t-16-36q-36-32-70 0-50 48-132-34l-148-146q-26-26-26-64t26-62q26-26 63-26t63 26z m450 574q56-56 56-132 0-78-56-134l-158-158q-74-72-150-72-62 0-112 50-14 14-14 34t14 36q14 14 35 14t35-14q50-48 122 24l158 156q28 28 28 64 0 38-28 62-24 26-56 31t-60-21l-50-50q-16-14-36-14t-34 14q-34 34 0 70l50 50q54 54 127 51t129-61z" horiz-adv-x="800" />
@ -44,6 +44,8 @@
<glyph glyph-name="attention-circled" unicode="&#xe812;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m71-696v106q0 8-5 13t-12 5h-107q-8 0-13-5t-6-13v-106q0-8 6-13t13-6h107q7 0 12 6t5 13z m-1 192l10 346q0 7-6 10-5 5-13 5h-123q-8 0-13-5-6-3-6-10l10-346q0-6 5-10t14-4h103q8 0 13 4t6 10z" horiz-adv-x="857.1" /> <glyph glyph-name="attention-circled" unicode="&#xe812;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m71-696v106q0 8-5 13t-12 5h-107q-8 0-13-5t-6-13v-106q0-8 6-13t13-6h107q7 0 12 6t5 13z m-1 192l10 346q0 7-6 10-5 5-13 5h-123q-8 0-13-5-6-3-6-10l10-346q0-6 5-10t14-4h103q8 0 13 4t6 10z" horiz-adv-x="857.1" />
<glyph glyph-name="download-alt" unicode="&#xe813;" d="M0-150l0 135 1000 0 0-135-1000 0z m88 586l228 0 0 414 370 0 0-414 228 0-414-385z" horiz-adv-x="1000" />
<glyph glyph-name="circle" unicode="&#xf111;" d="M857 350q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> <glyph glyph-name="circle" unicode="&#xf111;" d="M857 350q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="info" unicode="&#xf128;" d="M393 149v-134q0-9-7-15t-15-7h-134q-9 0-16 7t-7 15v134q0 9 7 16t16 6h134q9 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 11t-6 20v26q0 46 37 87t79 60q33 16 47 32t14 42q0 24-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" /> <glyph glyph-name="info" unicode="&#xf128;" d="M393 149v-134q0-9-7-15t-15-7h-134q-9 0-16 7t-7 15v134q0 9 7 16t16 6h134q9 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 11t-6 20v26q0 46 37 87t79 60q33 16 47 32t14 42q0 24-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" />

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -305,7 +305,7 @@
margin-top: 4px; margin-top: 4px;
margin-bottom: -2px; margin-bottom: -2px;
.icon-attention { .icon-attention-circled {
margin-right: -3px; margin-right: -3px;
} }
} }

View file

@ -28,6 +28,40 @@ video {
} }
} }
.video-download {
position: absolute;
top: 8px;
right: 8px;
z-index: 2;
color: var(--accent);
text-decoration: none;
opacity: 0;
transition: opacity 0.2s;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7));
.icon-container {
margin: 0;
}
.icon-download-alt {
font-size: 18px;
}
}
.attachment:hover .video-download {
opacity: 1;
&:hover {
color: var(--fg_color);
}
}
@media (hover: none) {
.video-download {
opacity: 1;
}
}
.video-overlay { .video-overlay {
@include play-button; @include play-button;
background-color: $shadow; background-color: $shadow;

View file

@ -50,8 +50,8 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch" let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
buildHtml(head): buildHtml(head):
link(rel="stylesheet", type="text/css", href="/css/style.css?v=43") link(rel="stylesheet", type="text/css", href="/css/style.css?v=44")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=5") link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=7")
if theme.len > 0: if theme.len > 0:
link(rel="stylesheet", type="text/css", href=(&"/css/themes/{theme}.css")) link(rel="stylesheet", type="text/css", href=(&"/css/themes/{theme}.css"))
@ -119,7 +119,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
# this is last so images are also preloaded # this is last so images are also preloaded
# if this is done earlier, Chrome only preloads one image for some reason # if this is done earlier, Chrome only preloads one image for some reason
link(rel="preload", type="font/woff2", `as`="font", link(rel="preload", type="font/woff2", `as`="font",
href="/fonts/fontello.woff2?61663884", crossorigin="anonymous") href="/fonts/fontello.woff2?59696369", crossorigin="anonymous")
proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs; proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs;
titleText=""; desc=""; ogTitle=""; rss=""; video=""; titleText=""; desc=""; ogTitle=""; rss=""; video="";

View file

@ -97,6 +97,12 @@ proc renderVideoUnavailable(video: Video): VNode =
else: else:
p: text "This media is unavailable" p: text "This media is unavailable"
proc getVideoDownloadUrl(videoData: Video): string =
let mp4Vars = videoData.variants.filterIt(it.contentType == mp4)
if mp4Vars.len == 0: return ""
let best = mp4Vars.sortedByIt(it.bitrate)[^1].url
if best.startsWith("http"): getVidUrl(best) else: best
proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""; bigThumb=false): VNode = proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""; bigThumb=false): VNode =
let let
playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4 playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4
@ -127,6 +133,11 @@ proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""; bigThumb=fal
if videoData.durationMs > 0: if videoData.durationMs > 0:
tdiv(class="overlay-duration"): text getDuration(videoData) tdiv(class="overlay-duration"): text getDuration(videoData)
verbatim "</div>" verbatim "</div>"
if videoData.available:
let dlUrl = getVideoDownloadUrl(videoData)
if dlUrl.len > 0:
a(class="video-download", href=dlUrl, download="",
title="Download video"): icon "download-alt"
proc renderVideo*(video: Video; prefs: Prefs; path: string; bigThumb=false): VNode = proc renderVideo*(video: Video; prefs: Prefs; path: string; bigThumb=false): VNode =
let hasCardContent = video.description.len > 0 or video.title.len > 0 let hasCardContent = video.description.len > 0 or video.title.len > 0
@ -345,10 +356,10 @@ proc renderDisclosures*(tweet: Tweet): VNode =
buildHtml(tdiv(class="disclosures")): buildHtml(tdiv(class="disclosures")):
if tweet.isAI: if tweet.isAI:
span(data-disclosure="ai"): span(data-disclosure="ai"):
icon "attention", "Made with AI" icon "attention-circled", "Made with AI"
if tweet.isAd: if tweet.isAd:
span(data-disclosure="ad"): span(data-disclosure="ad"):
icon "attention", "Paid partnership (ad)" icon "attention-circled", "Paid partnership (ad)"
proc renderLocation*(tweet: Tweet): string = proc renderLocation*(tweet: Tweet): string =
let (place, url) = tweet.getLocation() let (place, url) = tweet.getLocation()