From 641598cc77ff4511b2729d88ba28e78cb4d100e1 Mon Sep 17 00:00:00 2001 From: Matthew Esposito <matt@matthew.science> Date: Thu, 6 Feb 2025 15:05:59 -0500 Subject: [PATCH] feat: smaller imports and exports --- Cargo.lock | 84 +++++++++++------------ Cargo.toml | 4 +- src/main.rs | 2 + src/settings.rs | 25 ++++++- src/utils.rs | 100 ++++++++++++++++++++++++++-- static/copy.js | 9 +++ static/style.css | 8 ++- templates/settings.html | 143 +++++++++++++++++++++++++--------------- 8 files changed, 266 insertions(+), 109 deletions(-) create mode 100644 static/copy.js diff --git a/Cargo.lock b/Cargo.lock index 6578d17..a29b750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,12 +71,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" -[[package]] -name = "anyhow" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" - [[package]] name = "arc-swap" version = "1.7.1" @@ -139,6 +133,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base2048" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71f4fe417e8cc3bb9b437dfa9290ce92bd2730ba5374719bdfd9147fbc8f17cd" + [[package]] name = "base64" version = "0.21.7" @@ -151,6 +151,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -319,18 +328,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" -[[package]] -name = "common-words-all" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a6ff47eb813c9e315610ceca0ddd247827e22f2cdadc4189e4676a81470c77" -dependencies = [ - "anyhow", - "csv", - "glob", - "serde", -] - [[package]] name = "cookie" version = "0.18.1" @@ -394,27 +391,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "csv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" -dependencies = [ - "memchr", -] - [[package]] name = "darling" version = "0.20.10" @@ -698,12 +674,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "globset" version = "0.4.15" @@ -1381,13 +1351,14 @@ version = "0.35.1" dependencies = [ "arc-swap", "async-recursion", + "base2048", "base64 0.22.1", + "bincode", "brotli", "build_html", "cached", "chrono", "clap", - "common-words-all", "cookie", "dotenvy", "fastrand", @@ -1403,6 +1374,7 @@ dependencies = [ "pretty_env_logger", "pulldown-cmark", "regex", + "revision", "rinja", "route-recognizer", "rss", @@ -1459,6 +1431,26 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "revision" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f53179a035f881adad8c4d58a2c599c6b4a8325b989c68d178d7a34d1b1e4c" +dependencies = [ + "revision-derive", +] + +[[package]] +name = "revision-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0ec466e5d8dca9965eb6871879677bef5590cf7525ad96cae14376efb75073" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ring" version = "0.17.8" diff --git a/Cargo.toml b/Cargo.toml index 759b148..c7b6d4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,12 +48,14 @@ arc-swap = "1.7.1" serde_json_path = "0.7.1" async-recursion = "1.1.1" pulldown-cmark = { version = "0.12.0", features = ["simd", "html"], default-features = false } -common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] } hyper-rustls = { version = "0.24.2", features = [ "http2" ] } tegen = "0.1.4" serde_urlencoded = "0.7.1" chrono = { version = "0.4.39", default-features = false, features = [ "std" ] } htmlescape = "0.3.1" +bincode = "1.3.3" +base2048 = "2.0.2" +revision = "0.10.0" [dev-dependencies] diff --git a/src/main.rs b/src/main.rs index 165f0cb..bcfcd51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -246,6 +246,7 @@ async fn main() { app .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("/commits.atom").get(|_| async move { proxy_commit_info().await }.boxed()); app.at("/instances.json").get(|_| async move { proxy_instances().await }.boxed()); @@ -284,6 +285,7 @@ async fn main() { // Configure settings app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed()); app.at("/settings/restore").get(|r| settings::restore(r).boxed()); + app.at("/settings/encoded-restore").post(|r| settings::encoded_restore(r).boxed()); app.at("/settings/update").get(|r| settings::update(r).boxed()); // RSS Subscriptions diff --git a/src/settings.rs b/src/settings.rs index 4b30da3..b577666 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -5,12 +5,13 @@ use std::collections::HashMap; // CRATES use crate::server::ResponseExt; use crate::subreddit::join_until_size_limit; -use crate::utils::{redirect, template, Preferences}; +use crate::utils::{deflate_decompress, redirect, template, Preferences}; use cookie::Cookie; use futures_lite::StreamExt; use hyper::{Body, Request, Response}; use rinja::Template; use time::{Duration, OffsetDateTime}; +use url::form_urlencoded; // STRUCTS #[derive(Template)] @@ -262,3 +263,25 @@ pub async fn restore(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn update(req: Request<Body>) -> Result<Response<Body>, String> { Ok(set_cookies_method(req, false)) } + +pub async fn encoded_restore(req: Request<Body>) -> Result<Response<Body>, String> { + let body = hyper::body::to_bytes(req.into_body()) + .await + .map_err(|e| format!("Failed to get bytes from request body: {}", e))?; + + let encoded_prefs = form_urlencoded::parse(&body) + .find(|(key, _)| key == "encoded_prefs") + .map(|(_, value)| value) + .ok_or_else(|| "encoded_prefs parameter not found in request body".to_string())?; + + let bytes = base2048::decode(&encoded_prefs).ok_or_else(|| "Failed to decode base2048 encoded preferences".to_string())?; + + let out = deflate_decompress(bytes)?; + + let mut prefs: Preferences = bincode::deserialize(&out).map_err(|e| format!("Failed to deserialize bytes into Preferences struct: {}", e))?; + prefs.available_themes = vec![]; + + let url = format!("/settings/restore/?{}", prefs.to_urlencoded().unwrap()); + + Ok(redirect(&url)) +} diff --git a/src/utils.rs b/src/utils.rs index 2747449..73f608c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,16 +9,19 @@ use crate::{client::json, server::RequestExt}; use cookie::Cookie; use htmlescape::decode_html; use hyper::{Body, Request, Response}; +use libflate::deflate::{Decoder, Encoder}; use log::error; use once_cell::sync::Lazy; use regex::Regex; +use revision::revisioned; use rinja::Template; use rust_embed::RustEmbed; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use serde_json_path::{JsonPath, JsonPathExt}; use std::collections::{HashMap, HashSet}; use std::env; +use std::io::{Read, Write}; use std::str::FromStr; use std::string::ToString; use time::{macros::format_description, Duration, OffsetDateTime}; @@ -618,32 +621,55 @@ pub struct Params { pub before: Option<String>, } -#[derive(Default, Serialize)] +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[revisioned(revision = 1)] pub struct Preferences { - #[serde(skip)] + #[revision(start = 1)] + #[serde(skip_serializing, skip_deserializing)] pub available_themes: Vec<String>, + #[revision(start = 1)] pub theme: String, + #[revision(start = 1)] pub front_page: String, + #[revision(start = 1)] pub layout: String, + #[revision(start = 1)] pub wide: String, + #[revision(start = 1)] pub blur_spoiler: String, + #[revision(start = 1)] pub show_nsfw: String, + #[revision(start = 1)] pub blur_nsfw: String, + #[revision(start = 1)] pub hide_hls_notification: String, + #[revision(start = 1)] pub video_quality: String, + #[revision(start = 1)] pub hide_sidebar_and_summary: String, + #[revision(start = 1)] pub use_hls: String, + #[revision(start = 1)] pub autoplay_videos: String, + #[revision(start = 1)] pub fixed_navbar: String, + #[revision(start = 1)] pub disable_visit_reddit_confirmation: String, + #[revision(start = 1)] pub comment_sort: String, + #[revision(start = 1)] pub post_sort: String, - #[serde(serialize_with = "serialize_vec_with_plus")] + #[revision(start = 1)] + #[serde(serialize_with = "serialize_vec_with_plus", deserialize_with = "deserialize_vec_with_plus")] pub subscriptions: Vec<String>, - #[serde(serialize_with = "serialize_vec_with_plus")] + #[revision(start = 1)] + #[serde(serialize_with = "serialize_vec_with_plus", deserialize_with = "deserialize_vec_with_plus")] pub filters: Vec<String>, + #[revision(start = 1)] pub hide_awards: String, + #[revision(start = 1)] pub hide_score: String, + #[revision(start = 1)] pub remove_default_feeds: String, } @@ -654,6 +680,17 @@ where serializer.serialize_str(&vec.join("+")) } +fn deserialize_vec_with_plus<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> +where + D: Deserializer<'de>, +{ + let string = String::deserialize(deserializer)?; + if string.is_empty() { + return Ok(Vec::new()); + } + Ok(string.split('+').map(|s| s.to_string()).collect()) +} + #[derive(RustEmbed)] #[folder = "static/themes/"] #[include = "*.css"] @@ -698,6 +735,29 @@ impl Preferences { pub fn to_urlencoded(&self) -> Result<String, String> { serde_urlencoded::to_string(self).map_err(|e| e.to_string()) } + + pub fn to_bincode(&self) -> Result<Vec<u8>, String> { + bincode::serialize(self).map_err(|e| e.to_string()) + } + pub fn to_compressed_bincode(&self) -> Result<Vec<u8>, String> { + deflate_compress(self.to_bincode()?) + } + pub fn to_bincode_str(&self) -> Result<String, String> { + Ok(base2048::encode(&self.to_compressed_bincode()?)) + } +} + +pub fn deflate_compress(i: Vec<u8>) -> Result<Vec<u8>, String> { + let mut e = Encoder::new(Vec::new()); + e.write_all(&i).map_err(|e| e.to_string())?; + e.finish().into_result().map_err(|e| e.to_string()) +} + +pub fn deflate_decompress(i: Vec<u8>) -> Result<Vec<u8>, String> { + let mut decoder = Decoder::new(&i[..]); + let mut out = Vec::new(); + decoder.read_to_end(&mut out).map_err(|e| format!("Failed to read from gzip decoder: {}", e))?; + Ok(out) } /// Gets a `HashSet` of filters from the cookie in the given `Request`. @@ -1584,3 +1644,33 @@ How`s your monitor by the way? Any IPS bleed whatsoever? I either got lucky or t assert_eq!(render_bullet_lists(input), output); } + +#[test] +fn test_prefs_serialization_loop_json() { + let prefs = Preferences::default(); + let serialized = serde_json::to_string(&prefs).unwrap(); + let deserialized: Preferences = serde_json::from_str(&serialized).unwrap(); + assert_eq!(prefs, deserialized); +} + +#[test] +fn test_prefs_serialization_loop_bincode() { + let prefs = Preferences::default(); + let serialized = bincode::serialize(&prefs).unwrap(); + let deserialized: Preferences = bincode::deserialize(&serialized).unwrap(); + assert_eq!(prefs, deserialized); +} + +#[test] +fn test_known_good_configs() { + let configs = vec![ + "ఴӅβØØҞÉဏႢձĬ༧ȒʯऌԔӵ୮༏", + "ਧՊΥÀÃǎƱГ۸ඣമĖฤ႙ʟาúໜϾௐɥঀĜໃહཞઠѫҲɂఙ࿔DzઉƲӟӻĻฅΜδ໖ԜǗဖငƦơ৶Ą௩ԹʛใЛʃශаΏ", + "ਧԩΥÀÃΊ౭൩ඔႠϼҭöҪƸռઇԾॐნɔາǒՍҰच௨ಖມŃЉŐདƦ๙ϩএఠȝഽйʮჯඒϰळՋ௮ສ৵ऎΦѧਹಧଟƙŃ३î༦ŌပղयƟแҜ།", + ]; + for config in configs { + let bytes = base2048::decode(config).unwrap(); + let decompressed = deflate_decompress(bytes).unwrap(); + assert!(bincode::deserialize::<Preferences>(&decompressed).is_ok()); + } +} diff --git a/static/copy.js b/static/copy.js new file mode 100644 index 0000000..d7c7dd4 --- /dev/null +++ b/static/copy.js @@ -0,0 +1,9 @@ +async function copy() { + await navigator.clipboard.writeText(document.getElementById('bincode_str').value); +} + +async function set_listener() { + document.getElementById('copy').addEventListener('click', copy); +} + +window.addEventListener('load', set_listener); \ No newline at end of file diff --git a/static/style.css b/static/style.css index 76b55dd..545567e 100644 --- a/static/style.css +++ b/static/style.css @@ -553,14 +553,18 @@ aside { .subscribe, .unsubscribe, .filter, -.unfilter { +.unfilter, +.copy, +.import { padding: 10px 20px; border-radius: 5px; cursor: pointer; } .subscribe, -.filter { +.filter, +.copy, +.import { color: var(--foreground); background-color: var(--accent); } diff --git a/templates/settings.html b/templates/settings.html index 7995312..1995d47 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -4,22 +4,22 @@ {% block title %}Redlib Settings{% endblock %} {% block subscriptions %} - {% call utils::sub_list("") %} +{% call utils::sub_list("") %} {% endblock %} {% block search %} - {% call utils::search("".to_owned(), "") %} +{% call utils::search("".to_owned(), "") %} {% endblock %} {% block content %} - <div id="settings"> +<div id="settings"> <form action="/settings" method="POST"> <div class="prefs"> <fieldset> <legend>Appearance</legend> <div class="prefs-group"> <label for="theme">Theme:</label> - <select name="theme" id="theme"> + <select name="theme" id="theme"> {% call utils::options(prefs.theme, prefs.available_themes, "system") %} </select> </div> @@ -29,144 +29,162 @@ <div class="prefs-group"> <label for="remove_default_feeds">Remove default feeds</label> <input type="hidden" value="off" name="remove_default_feeds"> - <input type="checkbox" name="remove_default_feeds" id="remove_default_feeds" {% if prefs.remove_default_feeds == "on" %}checked{% endif %}> + <input type="checkbox" name="remove_default_feeds" id="remove_default_feeds" {% if + prefs.remove_default_feeds=="on" %}checked{% endif %}> </div> <div class="prefs-group"> <label for="front_page">Front page:</label> - <select name="front_page" id="front_page"> + <select name="front_page" id="front_page"> {% call utils::options(prefs.front_page, ["default", "popular", "all"], "default") %} </select> </div> <div class="prefs-group"> <label for="layout">Layout:</label> - <select name="layout" id="layout"> + <select name="layout" id="layout"> {% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %} </select> </div> <div class="prefs-group"> <label for="wide">Wide UI:</label> <input type="hidden" value="off" name="wide"> - <input type="checkbox" name="wide" id="wide" {% if prefs.wide == "on" %}checked{% endif %}> + <input type="checkbox" name="wide" id="wide" {% if prefs.wide=="on" %}checked{% endif %}> </div> </fieldset> <fieldset> <legend>Content</legend> <div class="prefs-group"> <label for="video_quality">Video quality:</label> - <select name="video_quality" id="video_quality"> + <select name="video_quality" id="video_quality"> {% call utils::options(prefs.video_quality, ["best", "medium", "worst"], "best") %} </select> </div> <div class="prefs-group"> <label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label> - <select name="post_sort"> - {% call utils::options(prefs.post_sort, ["hot", "new", "top", "rising", "controversial"], "hot") %} + <select name="post_sort"> + {% call utils::options(prefs.post_sort, ["hot", "new", "top", "rising", "controversial"], "hot") + %} </select> </div> <div class="prefs-group"> <label for="comment_sort">Default comment sort:</label> - <select name="comment_sort" id="comment_sort"> - {% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %} + <select name="comment_sort" id="comment_sort"> + {% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], + "confidence") %} </select> </div> <div class="prefs-group"> <label for="blur_spoiler">Blur spoiler previews:</label> <input type="hidden" value="off" name="blur_spoiler"> - <input type="checkbox" name="blur_spoiler" id="blur_spoiler" {% if prefs.blur_spoiler == "on" %}checked{% endif %}> + <input type="checkbox" name="blur_spoiler" id="blur_spoiler" {% if prefs.blur_spoiler=="on" + %}checked{% endif %}> </div> - {% if !crate::utils::sfw_only() %} + {% if !crate::utils::sfw_only() %} <div class="prefs-group"> <label for="show_nsfw">Show NSFW posts:</label> <input type="hidden" value="off" name="show_nsfw"> - <input type="checkbox" name="show_nsfw" id="show_nsfw" {% if prefs.show_nsfw == "on" %}checked{% endif %}> + <input type="checkbox" name="show_nsfw" id="show_nsfw" {% if prefs.show_nsfw=="on" %}checked{% endif + %}> </div> <div class="prefs-group"> <label for="blur_nsfw">Blur NSFW previews:</label> <input type="hidden" value="off" name="blur_nsfw"> - <input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}> + <input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw=="on" %}checked{% endif + %}> </div> - {% endif %} + {% endif %} <div class="prefs-group"> <label for="autoplay_videos">Autoplay videos</label> <input type="hidden" value="off" name="autoplay_videos"> - <input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}> + <input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos=="on" + %}checked{% endif %}> </div> <div class="prefs-group"> <label for="fixed_navbar">Keep navbar fixed</label> <input type="hidden" value="off" name="fixed_navbar"> - <input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar == "on" %}checked{% endif %}> + <input type="checkbox" name="fixed_navbar" {% if prefs.fixed_navbar=="on" %}checked{% endif %}> </div> <div class="prefs-group"> <label for="hide_sidebar_and_summary">Hide the summary and sidebar</label> <input type="hidden" value="off" name="hide_sidebar_and_summary"> - <input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary == "on" %}checked{% endif %}> + <input type="checkbox" name="hide_sidebar_and_summary" {% if prefs.hide_sidebar_and_summary=="on" + %}checked{% endif %}> </div> <div class="prefs-group"> <label for="use_hls">Use HLS for videos</label> <details id="feeds"> <summary>Why?</summary> - <div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or utilize this feature.</div> + <div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled + to be played with audio. Therefore, this toggle lets you either use Redlib JS-free or + utilize this feature.</div> </details> <input type="hidden" value="off" name="use_hls"> - <input type="checkbox" name="use_hls" id="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}> + <input type="checkbox" name="use_hls" id="use_hls" {% if prefs.use_hls=="on" %}checked{% endif %}> </div> <div class="prefs-group"> <label for="hide_hls_notification">Hide notification about possible HLS usage</label> <input type="hidden" value="off" name="hide_hls_notification"> - <input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}> + <input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if + prefs.hide_hls_notification=="on" %}checked{% endif %}> </div> <div class="prefs-group"> <label for="hide_awards">Hide awards</label> <input type="hidden" value="off" name="hide_awards"> - <input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards == "on" %}checked{% endif %}> + <input type="checkbox" name="hide_awards" id="hide_awards" {% if prefs.hide_awards=="on" %}checked{% + endif %}> </div> <div class="prefs-group"> <label for="hide_score">Hide score</label> <input type="hidden" value="off" name="hide_score"> - <input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score == "on" %}checked{% endif %}> + <input type="checkbox" name="hide_score" id="hide_score" {% if prefs.hide_score=="on" %}checked{% + endif %}> </div> <div class="prefs-group"> - <label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on Reddit</label> + <label for="disable_visit_reddit_confirmation">Do not confirm before visiting content on + Reddit</label> <input type="hidden" value="off" name="disable_visit_reddit_confirmation"> - <input type="checkbox" name="disable_visit_reddit_confirmation" {% if prefs.disable_visit_reddit_confirmation == "on" %}checked{% endif %}> + <input type="checkbox" name="disable_visit_reddit_confirmation" {% if + prefs.disable_visit_reddit_confirmation=="on" %}checked{% endif %}> </div> </fieldset> <input id="save" type="submit" value="Save"> </div> </form> {% if prefs.subscriptions.len() > 0 %} - <div class="prefs" id="settings_subs"> - <legend>Subscribed Feeds</legend> - {% for sub in prefs.subscriptions %} - <div> - {% let feed -%} - {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = format!("r/{}", sub) -%}{% endif -%} - <a href="/{{ feed }}">{{ feed }}</a> - <form action="/r/{{ sub }}/unsubscribe/?redirect=settings" method="POST"> - <button class="unsubscribe">Unsubscribe</button> - </form> - </div> - {% endfor %} + <div class="prefs" id="settings_subs"> + <legend>Subscribed Feeds</legend> + {% for sub in prefs.subscriptions %} + <div> + {% let feed -%} + {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = + format!("r/{}", sub) -%}{% endif -%} + <a href="/{{ feed }}">{{ feed }}</a> + <form action="/r/{{ sub }}/unsubscribe/?redirect=settings" method="POST"> + <button class="unsubscribe">Unsubscribe</button> + </form> </div> + {% endfor %} + </div> {% endif %} {% if !prefs.filters.is_empty() %} - <div class="prefs" id="settings_filters"> - <legend>Filtered Feeds</legend> - {% for sub in prefs.filters %} - <div> - {% let feed -%} - {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = format!("r/{}", sub) -%}{% endif -%} - <a href="/{{ feed }}">{{ feed }}</a> - <form action="/r/{{ sub }}/unfilter/?redirect=settings" method="POST"> - <button class="unfilter">Unfilter</button> - </form> - </div> - {% endfor %} + <div class="prefs" id="settings_filters"> + <legend>Filtered Feeds</legend> + {% for sub in prefs.filters %} + <div> + {% let feed -%} + {% if sub.starts_with("u_") -%}{% let feed = format!("u/{}", &sub[2..]) -%}{% else -%}{% let feed = + format!("r/{}", sub) -%}{% endif -%} + <a href="/{{ feed }}">{{ feed }}</a> + <form action="/r/{{ sub }}/unfilter/?redirect=settings" method="POST"> + <button class="unfilter">Unfilter</button> + </form> </div> + {% endfor %} + </div> {% endif %} <div id="settings_note"> - <p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p> + <p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them. + </p> <br> {% match prefs.to_urlencoded() %} {% when Ok with (encoded_prefs) %} @@ -176,7 +194,24 @@ <p>There was an error creating your restore link: {{ err }}</p> <p>Please report this issue</p> {% endmatch %} + <br /> + <div> + <script src="/copy.js"></script> + <label for="bincode_str">Or, export/import here (be sure to save ):</label> + <br /> + <input type="text" id="bincode_str" name="bincode_str" + value="{% match prefs.to_bincode_str() %}{% when Ok with (bincode_str) %}{{ bincode_str }}{% when Err with (err) %}{{ err }}{% endmatch %}" + readonly> + <button id="copy" class="copy">Copy</button> + + <br /> + <form action="/settings/encoded-restore/" method="POST"> + <input type="text" id="encoded_prefs" name="encoded_prefs" value="" + placeholder="Paste your encoded settings here"> + <button class="import" type="submit">Import</button> + </form> + </div> </div> </div> -{% endblock %} +{% endblock %} \ No newline at end of file