Compare commits
19 Commits
4099bbe732
...
f6665b6b6e
| Author | SHA1 | Date | |
|---|---|---|---|
| f6665b6b6e | |||
| ee93d725ba | |||
| 70fb635eda | |||
| b9fbefe05c | |||
| 46f823baae | |||
| cc1e998ec5 | |||
| fb73d8272e | |||
| 87321fb669 | |||
| 44b60d5070 | |||
| 89897aa48f | |||
| b2879211e4 | |||
| 6b3567fb1b | |||
| c27bcac549 | |||
| 25d31a6ce7 | |||
| ea280dd366 | |||
| 9842c8c99c | |||
| 906ebd73b2 | |||
| de95781ce7 | |||
| c58234fa2e |
@ -3,7 +3,6 @@
|
|||||||
rustflags = [ "--cfg=web_sys_unstable_apis" ]
|
rustflags = [ "--cfg=web_sys_unstable_apis" ]
|
||||||
|
|
||||||
[registry]
|
[registry]
|
||||||
default = "xinu"
|
|
||||||
global-credential-providers = ["cargo:token"]
|
global-credential-providers = ["cargo:token"]
|
||||||
|
|
||||||
[registries.xinu]
|
[registries.xinu]
|
||||||
|
|||||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -2910,7 +2910,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "letterbox"
|
name = "letterbox"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"build-info",
|
"build-info",
|
||||||
"build-info-build",
|
"build-info-build",
|
||||||
@ -2936,7 +2936,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "letterbox-server"
|
name = "letterbox-server"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -3455,7 +3455,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notmuch"
|
name = "notmuch"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
@ -4250,7 +4250,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "procmail2notmuch"
|
name = "procmail2notmuch"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
]
|
]
|
||||||
@ -5329,7 +5329,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shared"
|
name = "shared"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"build-info",
|
"build-info",
|
||||||
"notmuch",
|
"notmuch",
|
||||||
|
|||||||
15
Cargo.toml
15
Cargo.toml
@ -1,15 +1,10 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
default-members = [
|
default-members = ["server"]
|
||||||
"server"
|
members = ["web", "server", "notmuch", "procmail2notmuch", "shared"]
|
||||||
]
|
|
||||||
members = [
|
[profile.dev]
|
||||||
"web",
|
opt-level = 1
|
||||||
"server",
|
|
||||||
"notmuch",
|
|
||||||
"procmail2notmuch",
|
|
||||||
"shared"
|
|
||||||
]
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "notmuch"
|
name = "notmuch"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "procmail2notmuch"
|
name = "procmail2notmuch"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "letterbox-server"
|
name = "letterbox-server"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "letterbox-server"
|
default-run = "letterbox-server"
|
||||||
|
|
||||||
|
|||||||
@ -11,4 +11,4 @@ port = 9345
|
|||||||
#log_level = "critical"
|
#log_level = "critical"
|
||||||
newsreader_database_url = "postgres://newsreader@nixos-07.h.xinu.tv/newsreader"
|
newsreader_database_url = "postgres://newsreader@nixos-07.h.xinu.tv/newsreader"
|
||||||
newsreader_tantivy_db_path = "../target/database/newsreader"
|
newsreader_tantivy_db_path = "../target/database/newsreader"
|
||||||
slurp_cache_path = "/net/nasx/x/letterbox/slurp"
|
slurp_cache_path = "/tmp/letterbox/slurp"
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
DROP INDEX nzb_posts_created_at_idx;
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
CREATE INDEX nzb_posts_created_at_idx ON nzb_posts USING btree (created_at);
|
||||||
@ -1,4 +1,3 @@
|
|||||||
use chrono::NaiveDateTime;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use letterbox_server::mail::read_mail_to_db;
|
use letterbox_server::mail::read_mail_to_db;
|
||||||
use sqlx::postgres::PgPool;
|
use sqlx::postgres::PgPool;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
|||||||
pre {
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background-color: var(--color-bg-secondary);
|
|
||||||
}
|
|
||||||
@ -110,6 +110,29 @@ impl Transformer for StripHtml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct InlineRemoteStyle<'a> {
|
||||||
|
base_url: &'a Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> Transformer for InlineRemoteStyle<'a> {
|
||||||
|
async fn transform(&self, _: &Option<Url>, html: &str) -> Result<String, TransformError> {
|
||||||
|
//info!("HTML:\n{html}");
|
||||||
|
Ok(
|
||||||
|
match CSSInliner::options()
|
||||||
|
.base_url(self.base_url.clone())
|
||||||
|
.build()
|
||||||
|
.inline(&html)
|
||||||
|
{
|
||||||
|
Ok(inlined_html) => inlined_html,
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to inline remote CSS: {err}");
|
||||||
|
html.to_string()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
struct InlineStyle;
|
struct InlineStyle;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -118,10 +141,10 @@ impl Transformer for InlineStyle {
|
|||||||
let css = concat!(
|
let css = concat!(
|
||||||
"/* chrome-default.css */\n",
|
"/* chrome-default.css */\n",
|
||||||
include_str!("chrome-default.css"),
|
include_str!("chrome-default.css"),
|
||||||
"\n/* mvp.css */\n",
|
//"\n/* mvp.css */\n",
|
||||||
include_str!("mvp.css"),
|
//include_str!("mvp.css"),
|
||||||
"\n/* Xinu Specific overrides */\n",
|
//"\n/* Xinu Specific overrides */\n",
|
||||||
include_str!("custom.css"),
|
//include_str!("custom.css"),
|
||||||
);
|
);
|
||||||
let inline_opts = InlineOptions {
|
let inline_opts = InlineOptions {
|
||||||
inline_style_tags: true,
|
inline_style_tags: true,
|
||||||
@ -229,6 +252,7 @@ impl Transformer for AddOutlink {
|
|||||||
|
|
||||||
struct SlurpContents {
|
struct SlurpContents {
|
||||||
cacher: Arc<Mutex<FilesystemCacher>>,
|
cacher: Arc<Mutex<FilesystemCacher>>,
|
||||||
|
inline_css: bool,
|
||||||
site_selectors: HashMap<String, Vec<Selector>>,
|
site_selectors: HashMap<String, Vec<Selector>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,13 +269,25 @@ impl SlurpContents {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Transformer for SlurpContents {
|
impl Transformer for SlurpContents {
|
||||||
fn should_run(&self, link: &Option<Url>, _: &str) -> bool {
|
fn should_run(&self, link: &Option<Url>, html: &str) -> bool {
|
||||||
|
let mut will_slurp = false;
|
||||||
if let Some(link) = link {
|
if let Some(link) = link {
|
||||||
return self.get_selectors(link).is_some();
|
will_slurp = self.get_selectors(link).is_some();
|
||||||
}
|
}
|
||||||
false
|
if !will_slurp && self.inline_css {
|
||||||
|
return InlineStyle {}.should_run(link, html);
|
||||||
|
}
|
||||||
|
will_slurp
|
||||||
}
|
}
|
||||||
async fn transform(&self, link: &Option<Url>, html: &str) -> Result<String, TransformError> {
|
async fn transform(&self, link: &Option<Url>, html: &str) -> Result<String, TransformError> {
|
||||||
|
if let Some(test_link) = link {
|
||||||
|
// If SlurpContents is configured for inline CSS, but no
|
||||||
|
// configuration found for this site, use the local InlineStyle
|
||||||
|
// transform.
|
||||||
|
if self.inline_css && self.get_selectors(test_link).is_none() {
|
||||||
|
return InlineStyle {}.transform(link, html).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
let Some(link) = link else {
|
let Some(link) = link else {
|
||||||
return Ok(html.to_string());
|
return Ok(html.to_string());
|
||||||
};
|
};
|
||||||
@ -260,13 +296,51 @@ impl Transformer for SlurpContents {
|
|||||||
};
|
};
|
||||||
let cacher = self.cacher.lock().await;
|
let cacher = self.cacher.lock().await;
|
||||||
let body = if let Some(body) = cacher.get(link.as_str()) {
|
let body = if let Some(body) = cacher.get(link.as_str()) {
|
||||||
info!("cache hit for {link}");
|
|
||||||
String::from_utf8_lossy(&body).to_string()
|
String::from_utf8_lossy(&body).to_string()
|
||||||
} else {
|
} else {
|
||||||
let body = reqwest::get(link.as_str()).await?.text().await?;
|
let body = reqwest::get(link.as_str()).await?.text().await?;
|
||||||
cacher.set(link.as_str(), body.as_bytes());
|
cacher.set(link.as_str(), body.as_bytes());
|
||||||
body
|
body
|
||||||
};
|
};
|
||||||
|
let body = Arc::new(body);
|
||||||
|
let base_url = Some(link.clone());
|
||||||
|
let body = if self.inline_css {
|
||||||
|
let inner_body = Arc::clone(&body);
|
||||||
|
let res = tokio::task::spawn_blocking(move || {
|
||||||
|
let css = concat!(
|
||||||
|
"/* chrome-default.css */\n",
|
||||||
|
include_str!("chrome-default.css"),
|
||||||
|
"\n/* vars.css */\n",
|
||||||
|
include_str!("../../web/static/vars.css"),
|
||||||
|
//"\n/* Xinu Specific overrides */\n",
|
||||||
|
//include_str!("custom.css"),
|
||||||
|
);
|
||||||
|
let res = CSSInliner::options()
|
||||||
|
.base_url(base_url)
|
||||||
|
.extra_css(Some(std::borrow::Cow::Borrowed(css)))
|
||||||
|
.build()
|
||||||
|
.inline(&inner_body);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(inlined_html) => inlined_html,
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to inline remote CSS: {err}");
|
||||||
|
Arc::into_inner(inner_body).expect("failed to take body out of Arc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match res {
|
||||||
|
Ok(inlined_html) => inlined_html,
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to spawn inline remote CSS: {err}");
|
||||||
|
Arc::into_inner(body).expect("failed to take body out of Arc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Arc::into_inner(body).expect("failed to take body out of Arc")
|
||||||
|
};
|
||||||
|
|
||||||
let doc = Html::parse_document(&body);
|
let doc = Html::parse_document(&body);
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
@ -277,7 +351,7 @@ impl Transformer for SlurpContents {
|
|||||||
//warn!("couldn't find '{:?}' in {}", selector, link);
|
//warn!("couldn't find '{:?}' in {}", selector, link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(results.join("<hr>"))
|
Ok(results.join("<br>"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use std::{fs::File, io, io::Read};
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
use mailparse::{
|
use mailparse::{
|
||||||
addrparse_header, dateparse, parse_mail, MailHeaderMap, MailParseError, ParsedMail,
|
addrparse_header, dateparse, parse_mail, MailHeaderMap, MailParseError, ParsedMail,
|
||||||
@ -63,8 +63,7 @@ pub async fn read_mail_to_db(pool: &PgPool, path: &str) -> Result<(), MailError>
|
|||||||
|
|
||||||
println!("Feed: {feed_id} Subject: {}", subject);
|
println!("Feed: {feed_id} Subject: {}", subject);
|
||||||
|
|
||||||
if let Some(m) = first_html(&m) {
|
if let Some(_m) = first_html(&m) {
|
||||||
let body = m.get_body()?;
|
|
||||||
info!("add email {slug} {subject} {message_id} {date} {uid} {url}");
|
info!("add email {slug} {subject} {message_id} {date} {uid} {url}");
|
||||||
} else {
|
} else {
|
||||||
return Err(MailError::MissingHtmlPart.into());
|
return Err(MailError::MissingHtmlPart.into());
|
||||||
|
|||||||
@ -15,9 +15,8 @@ use crate::{
|
|||||||
config::Config,
|
config::Config,
|
||||||
error::ServerError,
|
error::ServerError,
|
||||||
graphql::{Corpus, NewsPost, Tag, Thread, ThreadSummary},
|
graphql::{Corpus, NewsPost, Tag, Thread, ThreadSummary},
|
||||||
thread_summary_from_row, AddOutlink, EscapeHtml, FrameImages, InlineStyle, Query, SanitizeHtml,
|
thread_summary_from_row, AddOutlink, FrameImages, Query, SanitizeHtml, SlurpContents,
|
||||||
SlurpContents, ThreadSummaryRecord, Transformer, NEWSREADER_TAG_PREFIX,
|
ThreadSummaryRecord, Transformer, NEWSREADER_TAG_PREFIX, NEWSREADER_THREAD_PREFIX,
|
||||||
NEWSREADER_THREAD_PREFIX,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn is_newsreader_query(query: &Query) -> bool {
|
pub fn is_newsreader_query(query: &Query) -> bool {
|
||||||
@ -189,7 +188,6 @@ pub async fn thread(
|
|||||||
|
|
||||||
let slug = r.site.unwrap_or("no-slug".to_string());
|
let slug = r.site.unwrap_or("no-slug".to_string());
|
||||||
let site = r.name.unwrap_or("NO SITE".to_string());
|
let site = r.name.unwrap_or("NO SITE".to_string());
|
||||||
let default_homepage = "http://no-homepage";
|
|
||||||
// TODO: remove the various places that have this as an Option
|
// TODO: remove the various places that have this as an Option
|
||||||
let link = Some(Url::parse(&r.link)?);
|
let link = Some(Url::parse(&r.link)?);
|
||||||
let mut body = r.summary.unwrap_or("NO SUMMARY".to_string());
|
let mut body = r.summary.unwrap_or("NO SUMMARY".to_string());
|
||||||
@ -197,6 +195,7 @@ pub async fn thread(
|
|||||||
let body_tranformers: Vec<Box<dyn Transformer>> = vec![
|
let body_tranformers: Vec<Box<dyn Transformer>> = vec![
|
||||||
Box::new(SlurpContents {
|
Box::new(SlurpContents {
|
||||||
cacher,
|
cacher,
|
||||||
|
inline_css: true,
|
||||||
site_selectors: hashmap![
|
site_selectors: hashmap![
|
||||||
"atmeta.com".to_string() => vec![
|
"atmeta.com".to_string() => vec![
|
||||||
Selector::parse("div.entry-content").unwrap(),
|
Selector::parse("div.entry-content").unwrap(),
|
||||||
@ -224,6 +223,9 @@ pub async fn thread(
|
|||||||
"ingowald.blog".to_string() => vec![
|
"ingowald.blog".to_string() => vec![
|
||||||
Selector::parse("article").unwrap(),
|
Selector::parse("article").unwrap(),
|
||||||
],
|
],
|
||||||
|
"jvns.ca".to_string() => vec![
|
||||||
|
Selector::parse("article").unwrap(),
|
||||||
|
],
|
||||||
"mitchellh.com".to_string() => vec![Selector::parse("div.w-full").unwrap()],
|
"mitchellh.com".to_string() => vec![Selector::parse("div.w-full").unwrap()],
|
||||||
"natwelch.com".to_string() => vec![
|
"natwelch.com".to_string() => vec![
|
||||||
Selector::parse("article div.prose").unwrap(),
|
Selector::parse("article div.prose").unwrap(),
|
||||||
@ -235,6 +237,17 @@ pub async fn thread(
|
|||||||
Selector::parse("span.story-byline").unwrap(),
|
Selector::parse("span.story-byline").unwrap(),
|
||||||
Selector::parse("div.p").unwrap(),
|
Selector::parse("div.p").unwrap(),
|
||||||
],
|
],
|
||||||
|
"theonion.com".to_string() => vec![
|
||||||
|
// Single cartoon
|
||||||
|
Selector::parse("article > div > div > figure").unwrap(),
|
||||||
|
// Image at top of article
|
||||||
|
Selector::parse("article > header > div > div > figure").unwrap(),
|
||||||
|
// Article body
|
||||||
|
Selector::parse("article .entry-content > *").unwrap(),
|
||||||
|
],
|
||||||
|
"trofi.github.io".to_string() => vec![
|
||||||
|
Selector::parse("#content").unwrap(),
|
||||||
|
],
|
||||||
"www.redox-os.org".to_string() => vec![
|
"www.redox-os.org".to_string() => vec![
|
||||||
Selector::parse("div.content").unwrap(),
|
Selector::parse("div.content").unwrap(),
|
||||||
],
|
],
|
||||||
@ -246,12 +259,12 @@ pub async fn thread(
|
|||||||
}),
|
}),
|
||||||
Box::new(FrameImages),
|
Box::new(FrameImages),
|
||||||
Box::new(AddOutlink),
|
Box::new(AddOutlink),
|
||||||
Box::new(EscapeHtml),
|
// TODO: causes doubling of images in cloudflare blogs
|
||||||
|
//Box::new(EscapeHtml),
|
||||||
Box::new(SanitizeHtml {
|
Box::new(SanitizeHtml {
|
||||||
cid_prefix: "",
|
cid_prefix: "",
|
||||||
base_url: &link,
|
base_url: &link,
|
||||||
}),
|
}),
|
||||||
Box::new(InlineStyle),
|
|
||||||
];
|
];
|
||||||
for t in body_tranformers.iter() {
|
for t in body_tranformers.iter() {
|
||||||
if t.should_run(&link, &body) {
|
if t.should_run(&link, &body) {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ use log::{error, info, warn};
|
|||||||
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
||||||
use memmap::MmapOptions;
|
use memmap::MmapOptions;
|
||||||
use notmuch::Notmuch;
|
use notmuch::Notmuch;
|
||||||
use rocket::http::uri::error::PathError;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
@ -224,7 +223,7 @@ pub async fn thread(
|
|||||||
}
|
}
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
r#"<p class="view-part-text-plain">{}</p>"#,
|
r#"<p class="view-part-text-plain font-mono whitespace-pre">{}</p>"#,
|
||||||
// Trim newlines to prevent excessive white space at the beginning/end of
|
// Trim newlines to prevent excessive white space at the beginning/end of
|
||||||
// presenation. Leave tabs and spaces incase plain text attempts to center a
|
// presenation. Leave tabs and spaces incase plain text attempts to center a
|
||||||
// header on the first line.
|
// header on the first line.
|
||||||
@ -579,7 +578,7 @@ fn flatten_body_parts(parts: &[Body]) -> Body {
|
|||||||
.map(|p| match p {
|
.map(|p| match p {
|
||||||
Body::PlainText(PlainText { text, .. }) => {
|
Body::PlainText(PlainText { text, .. }) => {
|
||||||
format!(
|
format!(
|
||||||
r#"<p class="view-part-text-plain">{}</p>"#,
|
r#"<p class="view-part-text-plain font-mono whitespace-pre">{}</p>"#,
|
||||||
// Trim newlines to prevent excessive white space at the beginning/end of
|
// Trim newlines to prevent excessive white space at the beginning/end of
|
||||||
// presenation. Leave tabs and spaces incase plain text attempts to center a
|
// presenation. Leave tabs and spaces incase plain text attempts to center a
|
||||||
// header on the first line.
|
// header on the first line.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "shared"
|
name = "shared"
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
version = "0.0.115"
|
version = "0.0.116"
|
||||||
name = "letterbox"
|
name = "letterbox"
|
||||||
repository = "https://github.com/seed-rs/seed-quickstart"
|
repository = "https://github.com/seed-rs/seed-quickstart"
|
||||||
authors = ["Bill Thiede <git@xinu.tv>"]
|
authors = ["Bill Thiede <git@xinu.tv>"]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[build]
|
[build]
|
||||||
release = true
|
release = false
|
||||||
|
|
||||||
[serve]
|
[serve]
|
||||||
# The address to serve on.
|
# The address to serve on.
|
||||||
|
|||||||
@ -4,22 +4,18 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<!--
|
|
||||||
-->
|
|
||||||
<link rel="stylesheet" href="https://jenil.github.io/bulmaswatch/cyborg/bulmaswatch.min.css">
|
|
||||||
<!-- Pretty checkboxes from https://justboil.github.io/bulma-checkbox/ -->
|
|
||||||
<link data-trunk rel="css" href="static/main.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
|
||||||
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
|
integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
<link rel="icon" href="https://static.xinu.tv/favicon/letterbox.svg" />
|
<link rel="icon" href="https://static.xinu.tv/favicon/letterbox.svg" />
|
||||||
<link data-trunk rel="css" href="static/style.css" />
|
|
||||||
<!-- tall thin font for user icon -->
|
<!-- tall thin font for user icon -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap" rel="stylesheet">
|
||||||
<link data-trunk rel="css" href="static/site-specific.css" />
|
<!-- <link data-trunk rel="css" href="static/site-specific.css" /> -->
|
||||||
|
<link data-trunk rel="css" href="static/vars.css" />
|
||||||
<link data-trunk rel="tailwind-css" href="./src/tailwind.css" />
|
<link data-trunk rel="tailwind-css" href="./src/tailwind.css" />
|
||||||
|
<link data-trunk rel="css" href="static/overrides.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -507,6 +507,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Msg::WindowScrolled => {
|
Msg::WindowScrolled => {
|
||||||
|
info!("WindowScrolled");
|
||||||
if let Some(el) = model.content_el.get() {
|
if let Some(el) = model.content_el.get() {
|
||||||
let ih = window()
|
let ih = window()
|
||||||
.inner_height()
|
.inner_height()
|
||||||
@ -515,6 +516,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
.value_of();
|
.value_of();
|
||||||
|
|
||||||
let r = el.get_bounding_client_rect();
|
let r = el.get_bounding_client_rect();
|
||||||
|
info!("r {r:?} ih {ih}");
|
||||||
if r.height() < ih {
|
if r.height() < ih {
|
||||||
// The whole content fits in the window, no scrollbar
|
// The whole content fits in the window, no scrollbar
|
||||||
orders.send_msg(Msg::SetProgress(0.));
|
orders.send_msg(Msg::SetProgress(0.));
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
use seed::{prelude::*, *};
|
|
||||||
use seed_hooks::topo;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
graphql::show_thread_query::*,
|
|
||||||
state::{Context, Model, Msg},
|
|
||||||
view::{self, reading_progress, view_header, view_search_results},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[topo::nested]
|
|
||||||
pub(super) fn view(model: &Model) -> Node<Msg> {
|
|
||||||
let show_icon_text = true;
|
|
||||||
// Do two queries, one without `unread` so it loads fast, then a second with unread.
|
|
||||||
let content = match &model.context {
|
|
||||||
Context::None => div![h1!["Loading"]],
|
|
||||||
Context::ThreadResult {
|
|
||||||
thread: ShowThreadQueryThread::EmailThread(thread),
|
|
||||||
open_messages,
|
|
||||||
} => view::thread(thread, open_messages, show_icon_text, &model.content_el),
|
|
||||||
Context::ThreadResult {
|
|
||||||
thread: ShowThreadQueryThread::NewsPost(post),
|
|
||||||
..
|
|
||||||
} => view::news_post(post, show_icon_text, &model.content_el),
|
|
||||||
Context::SearchResult {
|
|
||||||
query,
|
|
||||||
results,
|
|
||||||
count,
|
|
||||||
pager,
|
|
||||||
selected_threads,
|
|
||||||
} => view_search_results(
|
|
||||||
&query,
|
|
||||||
results.as_slice(),
|
|
||||||
*count,
|
|
||||||
pager,
|
|
||||||
selected_threads,
|
|
||||||
show_icon_text,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
div![
|
|
||||||
C!["main-content"],
|
|
||||||
reading_progress(model.read_completion_ratio),
|
|
||||||
div![view::tags(model), view::versions(&model.versions)],
|
|
||||||
div![
|
|
||||||
view_header(&model.query, &model.refreshing_state),
|
|
||||||
content,
|
|
||||||
view_header(&model.query, &model.refreshing_state),
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use seed::{prelude::*, *};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
api::urls,
|
|
||||||
graphql::{front_page_query::*, show_thread_query::*},
|
|
||||||
state::{Context, Model, Msg},
|
|
||||||
view::{
|
|
||||||
self, human_age, pretty_authors, reading_progress, search_toolbar, set_title, tags_chiclet,
|
|
||||||
view_header,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(super) fn view(model: &Model) -> Node<Msg> {
|
|
||||||
let show_icon_text = false;
|
|
||||||
let content = match &model.context {
|
|
||||||
Context::None => div![h1!["Loading"]],
|
|
||||||
Context::ThreadResult {
|
|
||||||
thread: ShowThreadQueryThread::EmailThread(thread),
|
|
||||||
open_messages,
|
|
||||||
} => view::thread(thread, open_messages, show_icon_text, &model.content_el),
|
|
||||||
Context::ThreadResult {
|
|
||||||
thread: ShowThreadQueryThread::NewsPost(post),
|
|
||||||
..
|
|
||||||
} => view::news_post(post, show_icon_text, &model.content_el),
|
|
||||||
Context::SearchResult {
|
|
||||||
query,
|
|
||||||
results,
|
|
||||||
count,
|
|
||||||
pager,
|
|
||||||
selected_threads,
|
|
||||||
} => search_results(
|
|
||||||
&query,
|
|
||||||
results.as_slice(),
|
|
||||||
*count,
|
|
||||||
pager,
|
|
||||||
selected_threads,
|
|
||||||
show_icon_text,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
div![
|
|
||||||
reading_progress(model.read_completion_ratio),
|
|
||||||
view_header(&model.query, &model.refreshing_state),
|
|
||||||
content,
|
|
||||||
view_header(&model.query, &model.refreshing_state),
|
|
||||||
div![view::tags(model), view::versions(&model.versions)]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_results(
|
|
||||||
query: &str,
|
|
||||||
results: &[FrontPageQuerySearchNodes],
|
|
||||||
count: usize,
|
|
||||||
pager: &FrontPageQuerySearchPageInfo,
|
|
||||||
selected_threads: &HashSet<String>,
|
|
||||||
show_icon_text: bool,
|
|
||||||
) -> Node<Msg> {
|
|
||||||
if query.is_empty() {
|
|
||||||
set_title("all mail");
|
|
||||||
} else {
|
|
||||||
set_title(query);
|
|
||||||
}
|
|
||||||
let rows = results.iter().map(|r| {
|
|
||||||
let tid = r.thread.clone();
|
|
||||||
let check_tid = r.thread.clone();
|
|
||||||
let datetime = human_age(r.timestamp as i64);
|
|
||||||
let unread_idx = r.tags.iter().position(|e| e == &"unread");
|
|
||||||
let mut tags = r.tags.clone();
|
|
||||||
if let Some(idx) = unread_idx {
|
|
||||||
tags.remove(idx);
|
|
||||||
};
|
|
||||||
div![
|
|
||||||
C!["row"],
|
|
||||||
label![
|
|
||||||
C!["b-checkbox", "checkbox", "is-large"],
|
|
||||||
input![attrs! {
|
|
||||||
At::Type=>"checkbox",
|
|
||||||
At::Checked=>selected_threads.contains(&tid).as_at_value(),
|
|
||||||
}],
|
|
||||||
span![C!["check"]],
|
|
||||||
ev(Ev::Input, move |e| {
|
|
||||||
if let Some(input) = e
|
|
||||||
.target()
|
|
||||||
.as_ref()
|
|
||||||
.expect("failed to get reference to target")
|
|
||||||
.dyn_ref::<web_sys::HtmlInputElement>()
|
|
||||||
{
|
|
||||||
if input.checked() {
|
|
||||||
Msg::SelectionAddThread(check_tid)
|
|
||||||
} else {
|
|
||||||
Msg::SelectionRemoveThread(check_tid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Msg::Noop
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
a![
|
|
||||||
C!["has-text-light", "summary"],
|
|
||||||
IF!(unread_idx.is_some() => C!["unread"]),
|
|
||||||
attrs! {
|
|
||||||
At::Href => urls::thread(&tid)
|
|
||||||
},
|
|
||||||
div![C!["subject"], &r.subject],
|
|
||||||
span![C!["from", "is-size-7"], pretty_authors(&r.authors)],
|
|
||||||
div![
|
|
||||||
span![C!["is-size-7"], tags_chiclet(&tags, true)],
|
|
||||||
span![C!["is-size-7", "float-right", "date"], datetime]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
});
|
|
||||||
let show_bulk_edit = !selected_threads.is_empty();
|
|
||||||
div![
|
|
||||||
C!["search-results"],
|
|
||||||
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
|
|
||||||
div![C!["index"], rows],
|
|
||||||
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,48 +0,0 @@
|
|||||||
use seed::{prelude::*, *};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
graphql::show_thread_query::*,
|
|
||||||
state::{Context, Model, Msg},
|
|
||||||
view::{self, reading_progress, view_header, view_search_results},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(super) fn view(model: &Model) -> Node<Msg> {
|
|
||||||
let show_icon_text = false;
|
|
||||||
// Do two queries, one without `unread` so it loads fast, then a second with unread.
|
|
||||||
let content = match &model.context {
|
|
||||||
Context::None => div![h1!["Loading"]],
|
|
||||||
Context::ThreadResult {
|
|
||||||
thread: ShowThreadQueryThread::EmailThread(thread),
|
|
||||||
open_messages,
|
|
||||||
} => view::thread(thread, open_messages, show_icon_text, &model.content_el),
|
|
||||||
Context::ThreadResult {
|
|
||||||
thread: ShowThreadQueryThread::NewsPost(post),
|
|
||||||
..
|
|
||||||
} => view::news_post(post, show_icon_text, &model.content_el),
|
|
||||||
Context::SearchResult {
|
|
||||||
query,
|
|
||||||
results,
|
|
||||||
count,
|
|
||||||
pager,
|
|
||||||
selected_threads,
|
|
||||||
} => view_search_results(
|
|
||||||
&query,
|
|
||||||
results.as_slice(),
|
|
||||||
*count,
|
|
||||||
pager,
|
|
||||||
selected_threads,
|
|
||||||
show_icon_text,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
div![
|
|
||||||
C!["main-content"],
|
|
||||||
div![
|
|
||||||
reading_progress(model.read_completion_ratio),
|
|
||||||
view_header(&model.query, &model.refreshing_state),
|
|
||||||
content,
|
|
||||||
view_header(&model.query, &model.refreshing_state),
|
|
||||||
view::tags(model),
|
|
||||||
view::versions(&model.versions)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,268 +0,0 @@
|
|||||||
/* Bulma Utilities */
|
|
||||||
.b-checkbox.checkbox {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Box-shadow on hover */
|
|
||||||
.b-checkbox.checkbox {
|
|
||||||
outline: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:not(.button) {
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:not(.button) + .checkbox:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox] {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0;
|
|
||||||
outline: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox] + .check {
|
|
||||||
width: 1.25em;
|
|
||||||
height: 1.25em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 2px solid #7a7a7a;
|
|
||||||
transition: background 150ms ease-out;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check {
|
|
||||||
background: #00d1b2 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #00d1b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-white {
|
|
||||||
background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%230a0a0a' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-black {
|
|
||||||
background: #0a0a0a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:white' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #0a0a0a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-light {
|
|
||||||
background: whitesmoke url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:rgba(0, 0, 0, 0.7)' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-dark {
|
|
||||||
background: #363636 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #363636;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-primary {
|
|
||||||
background: #00d1b2 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #00d1b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-link {
|
|
||||||
background: #485fc7 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #485fc7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-info {
|
|
||||||
background: #3e8ed0 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #3e8ed0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-success {
|
|
||||||
background: #48c78e url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #48c78e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-warning {
|
|
||||||
background: #ffe08a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:rgba(0, 0, 0, 0.7)' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #ffe08a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:checked + .check.is-danger {
|
|
||||||
background: #f14668 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #f14668;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check {
|
|
||||||
background: #00d1b2 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:%23fff' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #00d1b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-white, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-white {
|
|
||||||
background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:%230a0a0a' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-black, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-black {
|
|
||||||
background: #0a0a0a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:white' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #0a0a0a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-light, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-light {
|
|
||||||
background: whitesmoke url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:rgba(0, 0, 0, 0.7)' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-dark, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-dark {
|
|
||||||
background: #363636 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:%23fff' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #363636;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-primary, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-primary {
|
|
||||||
background: #00d1b2 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:%23fff' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #00d1b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-link, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-link {
|
|
||||||
background: #485fc7 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:%23fff' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #485fc7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-info, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-info {
|
|
||||||
background: #3e8ed0 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:%23fff' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #3e8ed0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-success, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-success {
|
|
||||||
background: #48c78e url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:%23fff' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #48c78e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-warning, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-warning {
|
|
||||||
background: #ffe08a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:rgba(0, 0, 0, 0.7)' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #ffe08a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:indeterminate + .check.is-danger, .b-checkbox.checkbox input[type=checkbox].is-indeterminate + .check.is-danger {
|
|
||||||
background: #f14668 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Crect style='fill:%23fff' width='0.7' height='0.2' x='.15' y='.4'%3E%3C/rect%3E%3C/svg%3E") no-repeat center center;
|
|
||||||
border-color: #f14668;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus + .check {
|
|
||||||
box-shadow: 0 0 0.5em rgba(122, 122, 122, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check {
|
|
||||||
box-shadow: 0 0 0.5em rgba(0, 209, 178, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-white {
|
|
||||||
box-shadow: 0 0 0.5em rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-black {
|
|
||||||
box-shadow: 0 0 0.5em rgba(10, 10, 10, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-light {
|
|
||||||
box-shadow: 0 0 0.5em rgba(245, 245, 245, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-dark {
|
|
||||||
box-shadow: 0 0 0.5em rgba(54, 54, 54, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-primary {
|
|
||||||
box-shadow: 0 0 0.5em rgba(0, 209, 178, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-link {
|
|
||||||
box-shadow: 0 0 0.5em rgba(72, 95, 199, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-info {
|
|
||||||
box-shadow: 0 0 0.5em rgba(62, 142, 208, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-success {
|
|
||||||
box-shadow: 0 0 0.5em rgba(72, 199, 142, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-warning {
|
|
||||||
box-shadow: 0 0 0.5em rgba(255, 224, 138, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox input[type=checkbox]:focus:checked + .check.is-danger {
|
|
||||||
box-shadow: 0 0 0.5em rgba(241, 70, 104, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox .control-label {
|
|
||||||
padding-left: calc(0.75em - 1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox.button {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox[disabled] {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check {
|
|
||||||
border-color: #00d1b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-white {
|
|
||||||
border-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-black {
|
|
||||||
border-color: #0a0a0a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-light {
|
|
||||||
border-color: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-dark {
|
|
||||||
border-color: #363636;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-primary {
|
|
||||||
border-color: #00d1b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-link {
|
|
||||||
border-color: #485fc7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-info {
|
|
||||||
border-color: #3e8ed0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-success {
|
|
||||||
border-color: #48c78e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-warning {
|
|
||||||
border-color: #ffe08a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox:hover input[type=checkbox]:not(:disabled) + .check.is-danger {
|
|
||||||
border-color: #f14668;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox.is-small {
|
|
||||||
border-radius: 2px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox.is-medium {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b-checkbox.checkbox.is-large {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
33
web/static/overrides.css
Normal file
33
web/static/overrides.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
html {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-post a {
|
||||||
|
color: var(--color-link) !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-post br {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1em;
|
||||||
|
content: " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-post h1,
|
||||||
|
.news-post h2,
|
||||||
|
.news-post h3,
|
||||||
|
.news-post h4 {
|
||||||
|
margin-top: 1em !important;
|
||||||
|
margin-bottom: 1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-post p {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-post pre,
|
||||||
|
.news-post code {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: #eee !important;
|
||||||
|
padding: 0.5em !important;
|
||||||
|
}
|
||||||
42
web/static/vars.css
Normal file
42
web/static/vars.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
:root {
|
||||||
|
--active-brightness: 0.85;
|
||||||
|
--border-radius: 5px;
|
||||||
|
--box-shadow: 2px 2px 10px;
|
||||||
|
--color-accent: #118bee15;
|
||||||
|
--color-bg: #fff;
|
||||||
|
--color-bg-secondary: #e9e9e9;
|
||||||
|
--color-link: #118bee;
|
||||||
|
--color-secondary: #920de9;
|
||||||
|
--color-secondary-accent: #920de90b;
|
||||||
|
--color-shadow: #f4f4f4;
|
||||||
|
--color-table: #118bee;
|
||||||
|
--color-text: #000;
|
||||||
|
--color-text-secondary: #999;
|
||||||
|
--color-scrollbar: #cacae8;
|
||||||
|
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
--hover-brightness: 1.2;
|
||||||
|
--justify-important: center;
|
||||||
|
--justify-normal: left;
|
||||||
|
--line-height: 1.5;
|
||||||
|
/*
|
||||||
|
--width-card: 285px;
|
||||||
|
--width-card-medium: 460px;
|
||||||
|
--width-card-wide: 800px;
|
||||||
|
*/
|
||||||
|
--width-content: 1080px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root[color-mode="user"] {
|
||||||
|
--color-accent: #0097fc4f;
|
||||||
|
--color-bg: #333;
|
||||||
|
--color-bg-secondary: #555;
|
||||||
|
--color-link: #0097fc;
|
||||||
|
--color-secondary: #e20de9;
|
||||||
|
--color-secondary-accent: #e20de94f;
|
||||||
|
--color-shadow: #bbbbbb20;
|
||||||
|
--color-table: #0097fc;
|
||||||
|
--color-text: #f7f7f7;
|
||||||
|
--color-text-secondary: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user