From 0b4d35f8beaa49d953a02e915c12322364e9ee31 Mon Sep 17 00:00:00 2001 From: LucifersCircle Date: Mon, 14 Sep 2026 21:53:21 -0700 Subject: [PATCH 1/4] Add selected upstream quality-of-life improvements Integrates redlib-org/redlib PRs #194, #291, #394, #506, #520, #521, #524, #539, #542, #546, #560, and #561. --- src/main.rs | 25 ++++++++++++- src/subreddit.rs | 2 +- src/utils.rs | 31 ++++++++++++++-- static/downloadGallery.js | 78 +++++++++++++++++++++++++++++++++++++++ static/playHLSVideo.js | 13 ++++++- static/style.css | 47 +++++++++++++++++++++-- templates/comment.html | 1 + templates/post.html | 3 +- templates/utils.html | 44 ++++++---------------- 9 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 static/downloadGallery.js diff --git a/src/main.rs b/src/main.rs index 399c0c9c0..62fea547d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -257,12 +257,16 @@ async fn main() { .at("/check_update.js") .get(|_| resource(include_str!("../static/check_update.js"), "text/javascript", false).boxed()); app.at("/copy.js").get(|_| resource(include_str!("../static/copy.js"), "text/javascript", false).boxed()); + app + .at("/downloadGallery.js") + .get(|_| resource(include_str!("../static/downloadGallery.js"), "text/javascript", false).boxed()); app.at("/commits.atom").get(|_| async move { proxy_commit_info().await }.boxed()); app.at("/instances.json").get(|_| async move { proxy_instances().await }.boxed()); // Proxy media through Redlib - app.at("/vid/:id/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed()); + app.at("/vid/:id/dash/:size").get(|r| proxy(r, "https://v.redd.it/{id}/DASH_{size}").boxed()); + app.at("/vid/:id/cmaf/:size").get(|r| proxy(r, "https://v.redd.it/{id}/CMAF_{size}").boxed()); app.at("/hls/:id/*path").get(|r| proxy(r, "https://v.redd.it/{id}/{path}").boxed()); app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed()); app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed()); @@ -276,6 +280,7 @@ async fn main() { app.at("/preview/:loc/:id").get(|r| proxy(r, "https://{loc}view.redd.it/{id}").boxed()); app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed()); app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed()); + app.at("/giphy/:id/:ext").get(|r| proxy(r, "https://media.giphy.com/media/{id}/giphy.{ext}").boxed()); // Browse user profile app @@ -372,7 +377,7 @@ async fn main() { Box::pin(async move { let sub = req.param("sub").unwrap_or_default(); match req.param("id").as_deref() { - // Share link + // Subreddit post share link Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/r/{sub}/s/{id}"), 3).await { Ok(Some(path)) => Ok(redirect(&path)), Ok(None) => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await, @@ -384,6 +389,22 @@ async fn main() { } }) }); + app.at("/u/:name/s/:id").get(|req: Request| { + Box::pin(async move { + let name = req.param("name").unwrap_or_default(); + match req.param("id").as_deref() { + // User post share link + Some(id) if (8..12).contains(&id.len()) => match canonical_path(format!("/u/{name}/s/{id}"), 3).await { + Ok(Some(path)) => Ok(redirect(&path)), + Ok(None) => error(req, "Post ID is invalid. It may point to a post on a user that has been banned.").await, + Err(e) => error(req, &e).await, + }, + + // Error message for unknown pages + _ => error(req, "Nothing here").await, + } + }) + }); app.at("/:id").get(|req: Request| { Box::pin(async move { diff --git a/src/subreddit.rs b/src/subreddit.rs index 348772504..bd4d13756 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -67,7 +67,7 @@ pub async fn community(req: Request) -> Result, String> { let subscribed = setting(&req, "subscriptions"); let front_page = setting(&req, "front_page"); let remove_default_feeds = setting(&req, "remove_default_feeds") == "on"; - let post_sort = req.cookie("post_sort").map_or_else(|| "hot".to_string(), |c| c.value().to_string()); + let post_sort = setting(&req, "post_sort"); let sort = req.param("sort").unwrap_or_else(|| req.param("id").unwrap_or(post_sort)); let sub_name = req.param("sub").unwrap_or(if front_page == "default" || front_page.is_empty() { diff --git a/src/utils.rs b/src/utils.rs index 36b798f44..7d6a0678e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1004,7 +1004,7 @@ static REGEX_URL_WWW: LazyLock = LazyLock::new(|| Regex::new(r"https?://w static REGEX_URL_OLD: LazyLock = LazyLock::new(|| Regex::new(r"https?://old\.reddit\.com/(.*)").unwrap()); static REGEX_URL_NP: LazyLock = LazyLock::new(|| Regex::new(r"https?://np\.reddit\.com/(.*)").unwrap()); static REGEX_URL_PLAIN: LazyLock = LazyLock::new(|| Regex::new(r"https?://reddit\.com/(.*)").unwrap()); -static REGEX_URL_VIDEOS: LazyLock = LazyLock::new(|| Regex::new(r"https?://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$|\?source=fallback))").unwrap()); +static REGEX_URL_VIDEOS: LazyLock = LazyLock::new(|| Regex::new(r"https?://v\.redd\.it/(.*)/(DASH|CMAF)_([0-9]{2,4}(\.mp4|$|\?source=fallback))").unwrap()); static REGEX_URL_VIDEOS_HLS: LazyLock = LazyLock::new(|| Regex::new(r"https?://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$").unwrap()); static REGEX_URL_IMAGES: LazyLock = LazyLock::new(|| Regex::new(r"https?://i\.redd\.it/(.*)").unwrap()); static REGEX_URL_THUMBS_A: LazyLock = LazyLock::new(|| Regex::new(r"https?://a\.thumbs\.redditmedia\.com/(.*)").unwrap()); @@ -1027,6 +1027,7 @@ pub fn format_url(url: &str) -> String { regex.captures(url).map_or(String::new(), |caps| match segments { 1 => [format, &caps[1]].join(""), 2 => [format, &caps[1], "/", &caps[2]].join(""), + 3 => [format, &caps[1], "/", &caps[2].to_lowercase().as_str(), "/", &caps[3]].join(""), _ => String::new(), }) }; @@ -1057,7 +1058,7 @@ pub fn format_url(url: &str) -> String { "old.reddit.com" => capture(®EX_URL_OLD, "/", 1), "np.reddit.com" => capture(®EX_URL_NP, "/", 1), "reddit.com" => capture(®EX_URL_PLAIN, "/", 1), - "v.redd.it" => chain!(capture(®EX_URL_VIDEOS, "/vid/", 2), capture(®EX_URL_VIDEOS_HLS, "/hls/", 2)), + "v.redd.it" => chain!(capture(®EX_URL_VIDEOS, "/vid/", 3), capture(®EX_URL_VIDEOS_HLS, "/hls/", 2)), "i.redd.it" => capture(®EX_URL_IMAGES, "/img/", 1), "a.thumbs.redditmedia.com" => capture(®EX_URL_THUMBS_A, "/thumb/a/", 1), "b.thumbs.redditmedia.com" => capture(®EX_URL_THUMBS_B, "/thumb/b/", 1), @@ -1158,6 +1159,24 @@ pub fn rewrite_urls(input_text: &str) -> String { } } +// Match Giphy URLs in comment anchor tags, capturing the GIF ID +// Handles: giphy.com/gifs/ID, media.giphy.com/media/ID, i.giphy.com/ID +static GIPHY_EMBED_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r#"(?i)]*>[^<]*"#).unwrap() +}); + +/// Rewrite Giphy URLs in comment body to embedded video elements +fn rewrite_giphy_links(comment: &str) -> String { + GIPHY_EMBED_REGEX + .replace_all(comment, |caps: ®ex::Captures| { + let id = &caps[1]; + format!( + r#"
"# + ) + }) + .to_string() +} + // These links all follow a pattern of "https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/SUBREDDIT_ID/RANDOM_FILENAME.png" static REDDIT_EMOTE_LINK_REGEX: LazyLock = LazyLock::new(|| Regex::new(r#"https://reddit-econ-prod-assets-permanent.s3.amazonaws.com/asset-manager/(.*)"#).unwrap()); @@ -1236,7 +1255,10 @@ pub fn rewrite_emotes(media_metadata: &Value, comment: String) -> String { comment = render_bullet_lists(&comment); // Call rewrite_urls() to transform any other Reddit links - rewrite_urls(&comment) + let comment = rewrite_urls(&comment); + + // Rewrite Giphy links to embedded videos + rewrite_giphy_links(&comment) } /// Format vote count to a string that will be displayed. @@ -1501,7 +1523,8 @@ mod tests { format_url("https://preview.redd.it/qwerty.jpg?auto=webp&s=asdf"), "/preview/pre/qwerty.jpg?auto=webp&s=asdf" ); - assert_eq!(format_url("https://v.redd.it/foo/DASH_360.mp4?source=fallback"), "/vid/foo/360.mp4"); + assert_eq!(format_url("https://v.redd.it/foo/DASH_360.mp4?source=fallback"), "/vid/foo/dash/360.mp4"); + assert_eq!(format_url("https://v.redd.it/foo/CMAF_720.mp4?source=fallback"), "/vid/foo/cmaf/720.mp4"); assert_eq!( format_url("https://v.redd.it/foo/HLSPlaylist.m3u8?a=bar&v=1&f=sd"), "/hls/foo/HLSPlaylist.m3u8?a=bar&v=1&f=sd" diff --git a/static/downloadGallery.js b/static/downloadGallery.js new file mode 100644 index 000000000..8bfdf33f6 --- /dev/null +++ b/static/downloadGallery.js @@ -0,0 +1,78 @@ +// Usage: downloadGallery(images, baseName) + +(function() { + // Add js-enabled class for CSS detection + document.body.classList.add('js-enabled'); + + window.downloadGallery = async function(images, baseName) { + // Request permission to download multiple files + // This will show a browser prompt asking for permission + try { + // Create a single reusable download link + const a = document.createElement('a'); + a.style.display = 'none'; + document.body.appendChild(a); + + // Use the File System Access API if available, otherwise fall back to individual downloads + for (let i = 0; i < images.length; i++) { + const imageUrl = images[i]; + const filename = `${baseName}_${i + 1}.jpg`; + + // Fetch the image as a blob + const response = await fetch(imageUrl); + const blob = await response.blob(); + + // Reuse the same link element + const url = URL.createObjectURL(blob); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + + // Small delay between downloads to avoid overwhelming the browser + if (i < images.length - 1) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + document.body.removeChild(a); + } catch (error) { + console.error('Error downloading gallery:', error); + alert('Failed to download some images. Please try downloading them individually.'); + } + }; + + // Attach click handlers to gallery download links + function attachDownloadHandlers() { + document.querySelectorAll('.download_gallery > a').forEach(function(link) { + link.addEventListener('click', function(e) { + e.preventDefault(); + + // Find all gallery images + const post = link.closest('.post'); + if (!post) return; + + const gallery = post.querySelector('.gallery'); + if (!gallery) return; + + const images = []; + gallery.querySelectorAll('figure img').forEach(function(img) { + images.push(img.src); + }); + + if (images.length === 0) return; + + // Get base name from data attribute + const baseName = link.getAttribute('data-base-name') || 'redlib_gallery'; + + downloadGallery(images, baseName); + }); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', attachDownloadHandlers); + } else { + attachDownloadHandlers(); + } +})(); diff --git a/static/playHLSVideo.js b/static/playHLSVideo.js index 01f680911..8142c5c9c 100644 --- a/static/playHLSVideo.js +++ b/static/playHLSVideo.js @@ -109,10 +109,21 @@ videoElement.parentNode.appendChild(qualitySelector); } + // IntersectionObserver to autoplay/pause video based on visibility + var autoplayObserver = new IntersectionObserver(function(entries) { + entries.forEach(function(entry) { + if (entry.isIntersecting) { + newVideo.play(); + } else { + newVideo.pause(); + } + }); + }, { threshold: 0.5 }); // Adjust the threshold as needed + newVideo.addEventListener('play', initializeHls); if (autoplay) { - newVideo.play(); + autoplayObserver.observe(newVideo); } }); } else { diff --git a/static/style.css b/static/style.css index c6dada03e..a897647aa 100644 --- a/static/style.css +++ b/static/style.css @@ -375,7 +375,7 @@ body > footer { align-items: center; width: 100%; background: var(--post); - position: absolute; + position: fixed; bottom: 0; } @@ -706,6 +706,10 @@ select, margin-bottom: 0; } +#searchbox:focus-within { + outline: 2px solid var(--accent); +} + #searchbox > *, #sort_submit { background: var(--highlighted); @@ -1308,6 +1312,14 @@ a.search_subreddit:hover { margin-right: 15px; } +#post_links li.download_gallery { + display: none; +} + +.js-enabled #post_links li.download_gallery { + display: list-item; +} + .desktop_item { display: auto; } @@ -1385,12 +1397,19 @@ a.search_subreddit:hover { display: flex; } +.giphy-embed-container, .comment img { max-width: 50%; height: auto; } +.giphy-embed { + width: 100%; + display: flex; +} + @media screen and (max-width: 500px) { + .giphy-embed-container, .comment img { max-width: 80%; height: auto; @@ -1450,6 +1469,19 @@ a.search_subreddit:hover { flex-grow: 1; flex-shrink: 1; min-width: 0; + position: relative; +} + +.comment_right[open] > .comment_data > .comment_collapse { + position: absolute; /* relative to .comment_right */ + width: 20px; + left: -35px; + top: 45px; + height: calc(100% - 45px); +} + +.comment:has(> .comment_right[open] > .comment_data > .comment_collapse:hover) > .comment_left * { + background: var(--highlighted); } .comment:has([id]) .comment_data > * { @@ -1978,7 +2010,7 @@ th { max-width: 100%; } #user { margin: 0 0 20px; } - + body.fixed_navbar { min-height: calc(100vh - 75px); padding-top: 45px; @@ -2063,6 +2095,14 @@ th { margin-top: -5px; } + .comment_right[open] > .comment_data > .comment_collapse { + /* relative to .comment_right */ + width: 15px; + left: -29px; + top: 35px; + height: calc(100% - 35px); + } + .line { margin-left: 5px; } @@ -2075,7 +2115,8 @@ th { margin-left: 12px; } .comment_data { - margin-left: 12px; + margin-left: 8px; + padding-left: 8px; } .user-comment .comment_data { diff --git a/templates/comment.html b/templates/comment.html index 3c008c78a..8ac5fa0d6 100644 --- a/templates/comment.html +++ b/templates/comment.html @@ -34,6 +34,7 @@ {% endfor %} {% endif %} +
{% if is_filtered %}
(Filtered content)
diff --git a/templates/post.html b/templates/post.html index adbe6d6da..478195303 100644 --- a/templates/post.html +++ b/templates/post.html @@ -2,7 +2,7 @@ {% import "utils.html" as utils %} {% block title %} - {% if single_thread %} + {% if single_thread && comments.len() > 0 %} {{ comments[0].author.name }} comments on {{ post.title }} - r/{{ post.community }} {% else %} {{ post.title }} - r/{{ post.community }} @@ -41,6 +41,7 @@ {% endif %} {% endif %} + {% endblock %} {% block subscriptions %} diff --git a/templates/utils.html b/templates/utils.html index 4cee97980..3758d5b2f 100644 --- a/templates/utils.html +++ b/templates/utils.html @@ -104,20 +104,7 @@

{% if post.post_type == "image" %}
- {% if post.media.height == 0 || post.media.width == 0 %} - Post image - {% else %} - - - - Post image - - - {% endif %}
{% else if post.post_type == "video" || post.post_type == "gif" %} @@ -186,7 +173,7 @@

{% endif %} {% call external_reddit_link(post.permalink) %} - {% if post.media.download_name != "" %} + {% if post.media.download_name != "" && post.post_type != "gallery" %}
  • dl @@ -194,6 +181,15 @@

  • {% endif %} + {% if post.post_type == "gallery" %} + + + {% endif %}

    {{ post.upvote_ratio }}% Upvoted

    @@ -253,20 +249,7 @@

    {% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
    - {% if post.media.height == 0 || post.media.width == 0 %} - Post image - {% else %} - - - - Post image - - - {% endif %}
    {% else if (prefs.layout.is_empty() || prefs.layout == "card") && (post.post_type == "gif" || post.post_type == "video") %} @@ -292,12 +275,7 @@

    {% else %}
    - - - - Thumbnail - - + Thumbnail
    {% endif %} {% if post.post_type == "link" %}{{ post.domain }}{% else %}{{ post.post_type }}{% endif %} From 3d973529b055684d4461d592f42a5f62a04f3513 Mon Sep 17 00:00:00 2001 From: LucifersCircle Date: Mon, 14 Sep 2026 21:56:16 -0700 Subject: [PATCH 2/4] Format existing RSS helpers Normalizes the upstream RSS code already present on main so the repository-wide rustfmt PR check passes. --- src/subreddit.rs | 53 ++++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/src/subreddit.rs b/src/subreddit.rs index bd4d13756..0b8341cbf 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -1,7 +1,8 @@ #![allow(clippy::cmp_owned)] use crate::utils::{ - Post, Preferences, Subreddit, catch_random, error, filter_posts, format_num, format_url, get_filters, info, nsfw_landing, param, redirect, rewrite_urls, setting, template, to_absolute_url, val + catch_random, error, filter_posts, format_num, format_url, get_filters, info, nsfw_landing, param, redirect, rewrite_urls, setting, template, to_absolute_url, val, Post, + Preferences, Subreddit, }; use crate::{client::json, server::RequestExt, server::ResponseExt}; use crate::{config, utils}; @@ -12,7 +13,7 @@ use hyper::{Body, Request, Response}; use chrono::DateTime; use regex::Regex; -use rss::{ChannelBuilder, Item, Enclosure}; +use rss::{ChannelBuilder, Enclosure, Item}; use std::sync::LazyLock; use time::{Duration, OffsetDateTime}; @@ -655,23 +656,13 @@ fn apply_enclosure(item: &mut Item, post: &Post) { // Embed the number of gallery images in description and content since // only the first image in the gallery is used for the enclosure if post.post_type == "gallery" && post.gallery.len() > 1 { - item.set_description( - format!("Gallery with {} images", - to_absolute_url(&post.permalink), - post.gallery.len() - ) - ); + item.set_description(format!("Gallery with {} images", to_absolute_url(&post.permalink), post.gallery.len())); if let Some(content) = item.content() { - let new_content = format!( - "{}
    {}", - item.description().unwrap_or(""), - content, - ); + let new_content = format!("{}
    {}", item.description().unwrap_or(""), content,); item.set_content(new_content); } } - } fn get_rss_image(post: &Post) -> Option { @@ -694,25 +685,21 @@ fn get_rss_image(post: &Post) -> Option { /// Determines the MIME type based on file extension in a URL. /// Handles both absolute and relative URLs with query parameters. fn get_mime_type(url: &str) -> &'static str { - // Extract the path component, removing query parameters - let path = url.split('?').next().unwrap_or(url); - - // Get the file extension (everything after the last dot) - let extension = path - .rsplit('.') - .next() - .unwrap_or("") - .to_lowercase(); - - // Match common image extensions - match extension.as_str() { - "jpg" | "jpeg" => "image/jpeg", - "png" => "image/png", - "gif" => "image/gif", - "webp" => "image/webp", - "svg" => "image/svg+xml", - _ => "application/octet-stream", - } + // Extract the path component, removing query parameters + let path = url.split('?').next().unwrap_or(url); + + // Get the file extension (everything after the last dot) + let extension = path.rsplit('.').next().unwrap_or("").to_lowercase(); + + // Match common image extensions + match extension.as_str() { + "jpg" | "jpeg" => "image/jpeg", + "png" => "image/png", + "gif" => "image/gif", + "webp" => "image/webp", + "svg" => "image/svg+xml", + _ => "application/octet-stream", + } } #[cfg(test)] From ee1d68765c9ce88652f4b0d831c5f4e199aba72a Mon Sep 17 00:00:00 2001 From: LucifersCircle Date: Mon, 14 Sep 2026 21:59:50 -0700 Subject: [PATCH 3/4] Satisfy current Clippy checks Applies behavior-preserving fixes for warnings promoted to errors by the pull-request workflow, including the imported CMAF URL formatter and pre-existing RSS/duplicates code. --- src/duplicates.rs | 2 +- src/subreddit.rs | 8 ++++---- src/utils.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/duplicates.rs b/src/duplicates.rs index 35f0d5a59..204651f64 100644 --- a/src/duplicates.rs +++ b/src/duplicates.rs @@ -176,7 +176,7 @@ pub async fn item(req: Request) -> Result, String> { let new_path: String = format!( "{}.json?before=t3_{}&sort={}&limit=1&raw_json=1", req.uri().path(), - &duplicates[0].id, + duplicates[0].id, if sort.is_empty() { "num_comments".to_string() } else { sort.clone() } ); match json(new_path, true).await { diff --git a/src/subreddit.rs b/src/subreddit.rs index 0b8341cbf..ab7e4ee1a 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -168,8 +168,8 @@ pub async fn community(req: Request) -> Result, String> { let no_posts = posts.is_empty(); let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"); if sort == "new" { - posts.sort_by(|a, b| b.created_ts.cmp(&a.created_ts)); - posts.sort_by(|a, b| b.flags.stickied.cmp(&a.flags.stickied)); + posts.sort_by_key(|post| std::cmp::Reverse(post.created_ts)); + posts.sort_by_key(|post| std::cmp::Reverse(post.flags.stickied)); } Ok(template(&SubredditTemplate { sub, @@ -651,7 +651,7 @@ pub async fn rss(req: Request) -> Result, String> { // Set enclosure image for RSS feed item fn apply_enclosure(item: &mut Item, post: &Post) { - item.set_enclosure(get_rss_image(&post)); + item.set_enclosure(get_rss_image(post)); // Embed the number of gallery images in description and content since // only the first image in the gallery is used for the enclosure @@ -668,7 +668,7 @@ fn apply_enclosure(item: &mut Item, post: &Post) { fn get_rss_image(post: &Post) -> Option { let image_url = match post.post_type.as_str() { "image" => Some(post.media.url.clone()), - "gallery" => post.gallery.get(0).and_then(|media| decode_html(&media.url).ok()), + "gallery" => post.gallery.first().and_then(|media| decode_html(&media.url).ok()), "gif" | "video" => decode_html(&post.media.poster).ok(), _ => None, }; diff --git a/src/utils.rs b/src/utils.rs index 7d6a0678e..af1bfd879 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1027,7 +1027,7 @@ pub fn format_url(url: &str) -> String { regex.captures(url).map_or(String::new(), |caps| match segments { 1 => [format, &caps[1]].join(""), 2 => [format, &caps[1], "/", &caps[2]].join(""), - 3 => [format, &caps[1], "/", &caps[2].to_lowercase().as_str(), "/", &caps[3]].join(""), + 3 => [format, &caps[1], "/", caps[2].to_lowercase().as_str(), "/", &caps[3]].join(""), _ => String::new(), }) }; From 465e07db74cf863c583acfddcf8cb7166ae632bf Mon Sep 17 00:00:00 2001 From: LucifersCircle Date: Mon, 14 Sep 2026 22:12:52 -0700 Subject: [PATCH 4/4] Link fork Docker image in README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3b7c6c3a5..4b1137b76 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Redlib +> [!NOTE] +> Docker images for this fork are published at [Docker Hub](https://hub.docker.com/r/luciferscircle/redlib): `docker.io/luciferscircle/redlib:latest`. + > An alternative private front-end to Reddit, with its origins in [Libreddit](https://github.com/libreddit/libreddit). ![screenshot](https://i.ibb.co/18vrdxk/redlib-rust.png)