Compare commits
No commits in common. "6b3567fb1b8d7a9ff0a099029683b2565a727499" and "4099bbe732f958bc89795754578cf44834dcebfa" have entirely different histories.
6b3567fb1b
...
4099bbe732
@ -3,6 +3,7 @@
|
|||||||
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]
|
||||||
|
|||||||
15
Cargo.toml
15
Cargo.toml
@ -1,10 +1,15 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
default-members = ["server"]
|
default-members = [
|
||||||
members = ["web", "server", "notmuch", "procmail2notmuch", "shared"]
|
"server"
|
||||||
|
]
|
||||||
[profile.dev]
|
members = [
|
||||||
opt-level = 1
|
"web",
|
||||||
|
"server",
|
||||||
|
"notmuch",
|
||||||
|
"procmail2notmuch",
|
||||||
|
"shared"
|
||||||
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
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;
|
||||||
|
|||||||
@ -110,48 +110,6 @@ 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> {
|
|
||||||
let css = concat!(
|
|
||||||
"/* chrome-default.css */\n",
|
|
||||||
include_str!("chrome-default.css"),
|
|
||||||
"\n/* mvp.css */\n",
|
|
||||||
include_str!("mvp.css"),
|
|
||||||
"\n/* Xinu Specific overrides */\n",
|
|
||||||
include_str!("custom.css"),
|
|
||||||
);
|
|
||||||
let inline_opts = InlineOptions {
|
|
||||||
//inline_style_tags: true,
|
|
||||||
//keep_style_tags: false,
|
|
||||||
//keep_link_tags: true,
|
|
||||||
base_url: self.base_url.clone(),
|
|
||||||
//load_remote_stylesheets: true,
|
|
||||||
//preallocate_node_capacity: 32,
|
|
||||||
..InlineOptions::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
//info!("HTML:\n{html}");
|
|
||||||
info!("base_url: {:#?}", self.base_url);
|
|
||||||
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]
|
||||||
@ -271,7 +229,6 @@ 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>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,36 +267,6 @@ impl Transformer for SlurpContents {
|
|||||||
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 res = CSSInliner::options()
|
|
||||||
.base_url(base_url)
|
|
||||||
.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();
|
||||||
@ -350,7 +277,7 @@ impl Transformer for SlurpContents {
|
|||||||
//warn!("couldn't find '{:?}' in {}", selector, link);
|
//warn!("couldn't find '{:?}' in {}", selector, link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(results.join("<br>"))
|
Ok(results.join("<hr>"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use std::{fs::File, io::Read};
|
use std::{fs::File, io, io::Read};
|
||||||
|
|
||||||
use mailparse::{
|
use mailparse::{
|
||||||
addrparse_header, dateparse, parse_mail, MailHeaderMap, MailParseError, ParsedMail,
|
addrparse_header, dateparse, parse_mail, MailHeaderMap, MailParseError, ParsedMail,
|
||||||
@ -63,7 +63,8 @@ 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,8 +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, InlineRemoteStyle, Query,
|
thread_summary_from_row, AddOutlink, EscapeHtml, FrameImages, InlineStyle, Query, SanitizeHtml,
|
||||||
SanitizeHtml, SlurpContents, ThreadSummaryRecord, Transformer, NEWSREADER_TAG_PREFIX,
|
SlurpContents, ThreadSummaryRecord, Transformer, NEWSREADER_TAG_PREFIX,
|
||||||
NEWSREADER_THREAD_PREFIX,
|
NEWSREADER_THREAD_PREFIX,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -189,6 +189,7 @@ 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());
|
||||||
@ -196,8 +197,6 @@ 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,
|
||||||
// TODO: make this true when bulma is finally removed
|
|
||||||
inline_css: false,
|
|
||||||
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(),
|
||||||
@ -225,9 +224,6 @@ 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(),
|
||||||
@ -239,9 +235,6 @@ 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(),
|
||||||
],
|
],
|
||||||
"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(),
|
||||||
],
|
],
|
||||||
@ -253,12 +246,12 @@ pub async fn thread(
|
|||||||
}),
|
}),
|
||||||
Box::new(FrameImages),
|
Box::new(FrameImages),
|
||||||
Box::new(AddOutlink),
|
Box::new(AddOutlink),
|
||||||
// TODO: causes doubling of images in cloudflare blogs
|
Box::new(EscapeHtml),
|
||||||
//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,6 +9,7 @@ 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;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[build]
|
[build]
|
||||||
release = false
|
release = true
|
||||||
|
|
||||||
[serve]
|
[serve]
|
||||||
# The address to serve on.
|
# The address to serve on.
|
||||||
|
|||||||
@ -4,15 +4,21 @@
|
|||||||
<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="tailwind-css" href="./src/tailwind.css" />
|
<link data-trunk rel="tailwind-css" href="./src/tailwind.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@ -507,7 +507,6 @@ 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()
|
||||||
@ -516,7 +515,6 @@ 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.));
|
||||||
|
|||||||
49
web/src/view/desktop.rs
Normal file
49
web/src/view/desktop.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
121
web/src/view/mobile.rs
Normal file
121
web/src/view/mobile.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
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),
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -12,140 +12,17 @@ use web_sys::HtmlElement;
|
|||||||
use crate::{
|
use crate::{
|
||||||
api::urls,
|
api::urls,
|
||||||
graphql::{front_page_query::*, show_thread_query::*},
|
graphql::{front_page_query::*, show_thread_query::*},
|
||||||
state::{unread_query, Context, Model, Msg, RefreshingState, Tag},
|
state::{unread_query, Model, Msg, RefreshingState, Tag},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod desktop;
|
||||||
|
mod mobile;
|
||||||
|
mod tablet;
|
||||||
|
|
||||||
// TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread
|
// TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread
|
||||||
// ids, and has a to_query_string() that knows notmuch's syntax. Then remove the smattering of
|
// ids, and has a to_query_string() that knows notmuch's syntax. Then remove the smattering of
|
||||||
// format!() calls all over with magic strings representing notmuch specific syntax.
|
// format!() calls all over with magic strings representing notmuch specific syntax.
|
||||||
const MAX_RAW_MESSAGE_SIZE: usize = 100_000;
|
const MAX_RAW_MESSAGE_SIZE: usize = 100_000;
|
||||||
|
|
||||||
pub fn view(model: &Model) -> Node<Msg> {
|
|
||||||
let show_icon_text = true;
|
|
||||||
let content = match &model.context {
|
|
||||||
Context::None => div![h1!["Loading"]],
|
|
||||||
Context::ThreadResult {
|
|
||||||
thread: ShowThreadQueryThread::EmailThread(thread_data),
|
|
||||||
open_messages,
|
|
||||||
} => thread(
|
|
||||||
thread_data,
|
|
||||||
open_messages,
|
|
||||||
show_icon_text,
|
|
||||||
&model.content_el,
|
|
||||||
),
|
|
||||||
Context::ThreadResult {
|
|
||||||
thread: ShowThreadQueryThread::NewsPost(post),
|
|
||||||
..
|
|
||||||
} => 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![
|
|
||||||
C!["flex", "flex-wrap-reverse"],
|
|
||||||
div![
|
|
||||||
C!["w-48", "flex-none", "flex", "flex-col"],
|
|
||||||
tags(model),
|
|
||||||
versions(&model.versions)
|
|
||||||
],
|
|
||||||
reading_progress(model.read_completion_ratio),
|
|
||||||
div![
|
|
||||||
C!["flex-auto"],
|
|
||||||
view_header(&model.query, &model.refreshing_state),
|
|
||||||
content,
|
|
||||||
view_header(&model.query, &model.refreshing_state),
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
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!["NOTPORTED", "row"],
|
|
||||||
label![
|
|
||||||
C!["NOTPORTED", "b-checkbox", "checkbox", "is-large"],
|
|
||||||
input![attrs! {
|
|
||||||
At::Type=>"checkbox",
|
|
||||||
At::Checked=>selected_threads.contains(&tid).as_at_value(),
|
|
||||||
}],
|
|
||||||
span![C!["NOTPORTED", "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!["NOTPORTED", "has-text-light", "summary"],
|
|
||||||
IF!(unread_idx.is_some() => C!["NOTPORTED","unread"]),
|
|
||||||
attrs! {
|
|
||||||
At::Href => urls::thread(&tid)
|
|
||||||
},
|
|
||||||
div![C!["NOTPORTED", "subject"], &r.subject],
|
|
||||||
span![
|
|
||||||
C!["NOTPORTED", "from", "is-size-7"],
|
|
||||||
pretty_authors(&r.authors)
|
|
||||||
],
|
|
||||||
div![
|
|
||||||
span![C!["NOTPORTED", "is-size-7"], tags_chiclet(&tags, true)],
|
|
||||||
span![
|
|
||||||
C!["NOTPORTED", "is-size-7", "float-right", "date"],
|
|
||||||
datetime
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
});
|
|
||||||
let show_bulk_edit = !selected_threads.is_empty();
|
|
||||||
div![
|
|
||||||
C!["NOTPORTED", "search-results"],
|
|
||||||
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
|
|
||||||
div![C!["NOTPORTED", "index"], rows],
|
|
||||||
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
fn set_title(title: &str) {
|
fn set_title(title: &str) {
|
||||||
seed::document().set_title(&format!("lb: {}", title));
|
seed::document().set_title(&format!("lb: {}", title));
|
||||||
}
|
}
|
||||||
@ -154,11 +31,11 @@ fn tags_chiclet(tags: &[String], is_mobile: bool) -> impl Iterator<Item = Node<M
|
|||||||
tags.iter().map(move |tag| {
|
tags.iter().map(move |tag| {
|
||||||
let hex = compute_color(tag);
|
let hex = compute_color(tag);
|
||||||
let style = style! {St::BackgroundColor=>hex};
|
let style = style! {St::BackgroundColor=>hex};
|
||||||
let classes = C!["NOTPORTED", "tag", IF!(is_mobile => "is-small")];
|
let classes = C!["tag", IF!(is_mobile => "is-small")];
|
||||||
let tag = tag.clone();
|
let tag = tag.clone();
|
||||||
a![match tag.as_str() {
|
a![match tag.as_str() {
|
||||||
"attachment" => span![classes, style, "📎"],
|
"attachment" => span![classes, style, "📎"],
|
||||||
"replied" => span![classes, style, i![C!["NOTPORTED", "fa-solid", "fa-reply"]]],
|
"replied" => span![classes, style, i![C!["fa-solid", "fa-reply"]]],
|
||||||
_ => span![classes, style, &tag],
|
_ => span![classes, style, &tag],
|
||||||
},]
|
},]
|
||||||
})
|
})
|
||||||
@ -180,23 +57,23 @@ fn removable_tags_chiclet<'a>(
|
|||||||
let thread_id = thread_id.to_string();
|
let thread_id = thread_id.to_string();
|
||||||
let hex = compute_color(tag);
|
let hex = compute_color(tag);
|
||||||
let style = style! {St::BackgroundColor=>hex};
|
let style = style! {St::BackgroundColor=>hex};
|
||||||
let classes = C!["NOTPORTED", "tag", IF!(is_mobile => "is-small")];
|
let classes = C!["tag", IF!(is_mobile => "is-small")];
|
||||||
let attrs = attrs! {
|
let attrs = attrs! {
|
||||||
At::Href => urls::search(&format!("tag:{tag}"), 0)
|
At::Href => urls::search(&format!("tag:{tag}"), 0)
|
||||||
};
|
};
|
||||||
let tag = tag.clone();
|
let tag = tag.clone();
|
||||||
let rm_tag = tag.clone();
|
let rm_tag = tag.clone();
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "control"],
|
C!["control"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "tags", "has-addons"],
|
C!["tags", "has-addons"],
|
||||||
a![
|
a![
|
||||||
classes,
|
classes,
|
||||||
attrs,
|
attrs,
|
||||||
style,
|
style,
|
||||||
match tag.as_str() {
|
match tag.as_str() {
|
||||||
"attachment" => span!["📎"],
|
"attachment" => span!["📎"],
|
||||||
"replied" => span![i![C!["NOTPORTED", "fa-solid", "fa-reply"]]],
|
"replied" => span![i![C!["fa-solid", "fa-reply"]]],
|
||||||
_ => span![&tag],
|
_ => span![&tag],
|
||||||
},
|
},
|
||||||
ev(Ev::Click, move |_| Msg::FrontPageRequest {
|
ev(Ev::Click, move |_| Msg::FrontPageRequest {
|
||||||
@ -208,7 +85,7 @@ fn removable_tags_chiclet<'a>(
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
a![
|
a![
|
||||||
C!["NOTPORTED", "tag", "is-delete"],
|
C!["tag", "is-delete"],
|
||||||
ev(Ev::Click, move |_| Msg::RemoveTag(thread_id, rm_tag))
|
ev(Ev::Click, move |_| Msg::RemoveTag(thread_id, rm_tag))
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -300,14 +177,14 @@ fn view_search_results(
|
|||||||
&r.subject
|
&r.subject
|
||||||
};
|
};
|
||||||
tr![
|
tr![
|
||||||
IF!(unread_idx.is_some() => C!["NOTPORTED","unread"]),
|
IF!(unread_idx.is_some() => C!["unread"]),
|
||||||
td![label![
|
td![label![
|
||||||
C!["NOTPORTED", "b-checkbox", "checkbox"],
|
C!["b-checkbox", "checkbox"],
|
||||||
input![attrs! {
|
input![attrs! {
|
||||||
At::Type=>"checkbox",
|
At::Type=>"checkbox",
|
||||||
At::Checked=>selected_threads.contains(&tid).as_at_value(),
|
At::Checked=>selected_threads.contains(&tid).as_at_value(),
|
||||||
}],
|
}],
|
||||||
span![C!["NOTPORTED", "check"]],
|
span![C!["check"]],
|
||||||
ev(Ev::Input, move |e| {
|
ev(Ev::Input, move |e| {
|
||||||
if let Some(input) = e
|
if let Some(input) = e
|
||||||
.target()
|
.target()
|
||||||
@ -326,9 +203,9 @@ fn view_search_results(
|
|||||||
}),
|
}),
|
||||||
]],
|
]],
|
||||||
td![
|
td![
|
||||||
C!["NOTPORTED", "from", format!("corpus-{:?} ", r.corpus)],
|
C!["from", format!("corpus-{:?} ", r.corpus)],
|
||||||
a![
|
a![
|
||||||
C!["NOTPORTED", "has-text-light", "text"],
|
C!["has-text-light", "text"],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Href => urls::thread(&tid)
|
At::Href => urls::thread(&tid)
|
||||||
},
|
},
|
||||||
@ -337,11 +214,11 @@ fn view_search_results(
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
td![
|
td![
|
||||||
C!["NOTPORTED", "subject"],
|
C!["subject"],
|
||||||
a![
|
a![
|
||||||
tags_chiclet(&tags, false),
|
tags_chiclet(&tags, false),
|
||||||
" ",
|
" ",
|
||||||
C!["NOTPORTED", "has-text-light", "text", "subject-link"],
|
C!["has-text-light", "text", "subject-link"],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Href => urls::thread(&tid)
|
At::Href => urls::thread(&tid)
|
||||||
},
|
},
|
||||||
@ -349,9 +226,9 @@ fn view_search_results(
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
td![
|
td![
|
||||||
C!["NOTPORTED", "date"],
|
C!["date"],
|
||||||
a![
|
a![
|
||||||
C!["NOTPORTED", "has-text-light", "text"],
|
C!["has-text-light", "text"],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Href => urls::thread(&tid)
|
At::Href => urls::thread(&tid)
|
||||||
},
|
},
|
||||||
@ -362,7 +239,7 @@ fn view_search_results(
|
|||||||
});
|
});
|
||||||
|
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "search-results"],
|
C!["search-results"],
|
||||||
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
|
search_toolbar(count, pager, show_bulk_edit, show_icon_text),
|
||||||
table![
|
table![
|
||||||
C![
|
C![
|
||||||
@ -375,17 +252,17 @@ fn view_search_results(
|
|||||||
],
|
],
|
||||||
thead![tr![
|
thead![tr![
|
||||||
th![
|
th![
|
||||||
C!["NOTPORTED", "edit"],
|
C!["edit"],
|
||||||
label![
|
label![
|
||||||
C!["NOTPORTED", "b-checkbox", "checkbox"],
|
C!["b-checkbox", "checkbox"],
|
||||||
input![
|
input![
|
||||||
IF!(partially_checked => C!["NOTPORTED","is-indeterminate"]),
|
IF!(partially_checked => C!["is-indeterminate"]),
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Type=>"checkbox",
|
At::Type=>"checkbox",
|
||||||
At::Checked=>all_checked.as_at_value(),
|
At::Checked=>all_checked.as_at_value(),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
span![C!["NOTPORTED", "check"]],
|
span![C!["check"]],
|
||||||
ev(Ev::Click, move |_| if all_checked {
|
ev(Ev::Click, move |_| if all_checked {
|
||||||
Msg::SelectionSetNone
|
Msg::SelectionSetNone
|
||||||
} else {
|
} else {
|
||||||
@ -393,9 +270,9 @@ fn view_search_results(
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
th![C!["NOTPORTED", "from"], "From"],
|
th![C!["from"], "From"],
|
||||||
th![C!["NOTPORTED", "subject"], "Subject"],
|
th![C!["subject"], "Subject"],
|
||||||
th![C!["NOTPORTED", "date"], "Date"]
|
th![C!["date"], "Date"]
|
||||||
]],
|
]],
|
||||||
tbody![rows]
|
tbody![rows]
|
||||||
],
|
],
|
||||||
@ -410,24 +287,24 @@ fn search_toolbar(
|
|||||||
show_icon_text: bool,
|
show_icon_text: bool,
|
||||||
) -> Node<Msg> {
|
) -> Node<Msg> {
|
||||||
nav![
|
nav![
|
||||||
C!["NOTPORTED", "level", "is-mobile"],
|
C!["level", "is-mobile"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "level-left"],
|
C!["level-left"],
|
||||||
IF!(show_bulk_edit =>
|
IF!(show_bulk_edit =>
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED","level-item"],
|
C!["level-item"],
|
||||||
div![C!["NOTPORTED","buttons", "has-addons"],
|
div![C!["buttons", "has-addons"],
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED","button", "mark-read"],
|
C!["button", "mark-read"],
|
||||||
attrs!{At::Title => "Mark as read"},
|
attrs!{At::Title => "Mark as read"},
|
||||||
span![C!["NOTPORTED","icon", "is-small"], i![C!["NOTPORTED","far", "fa-envelope-open"]]],
|
span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
|
||||||
IF!(show_icon_text=>span!["Read"]),
|
IF!(show_icon_text=>span!["Read"]),
|
||||||
ev(Ev::Click, |_| Msg::SelectionMarkAsRead)
|
ev(Ev::Click, |_| Msg::SelectionMarkAsRead)
|
||||||
],
|
],
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED","button", "mark-unread"],
|
C!["button", "mark-unread"],
|
||||||
attrs!{At::Title => "Mark as unread"},
|
attrs!{At::Title => "Mark as unread"},
|
||||||
span![C!["NOTPORTED","icon", "is-small"], i![C!["NOTPORTED","far", "fa-envelope"]]],
|
span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
|
||||||
IF!(show_icon_text=>span!["Unread"]),
|
IF!(show_icon_text=>span!["Unread"]),
|
||||||
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread)
|
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread)
|
||||||
]
|
]
|
||||||
@ -435,12 +312,12 @@ fn search_toolbar(
|
|||||||
]),
|
]),
|
||||||
IF!(show_bulk_edit =>
|
IF!(show_bulk_edit =>
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED","level-item"],
|
C!["level-item"],
|
||||||
div![C!["NOTPORTED","buttons", "has-addons"],
|
div![C!["buttons", "has-addons"],
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED","button", "spam"],
|
C!["button", "spam"],
|
||||||
attrs!{At::Title => "Mark as spam"},
|
attrs!{At::Title => "Mark as spam"},
|
||||||
span![C!["NOTPORTED","icon", "is-small"], i![C!["NOTPORTED","far", "fa-hand"]]],
|
span![C!["icon", "is-small"], i![C!["far", "fa-hand"]]],
|
||||||
IF!(show_icon_text=>span!["Spam"]),
|
IF!(show_icon_text=>span!["Spam"]),
|
||||||
ev(Ev::Click, |_|
|
ev(Ev::Click, |_|
|
||||||
Msg::MultiMsg(vec![
|
Msg::MultiMsg(vec![
|
||||||
@ -453,9 +330,9 @@ fn search_toolbar(
|
|||||||
])
|
])
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "level-right"],
|
C!["level-right"],
|
||||||
nav![
|
nav![
|
||||||
C!["NOTPORTED", "level-item", "pagination"],
|
C!["level-item", "pagination"],
|
||||||
a![
|
a![
|
||||||
C![
|
C![
|
||||||
"pagination-previous",
|
"pagination-previous",
|
||||||
@ -476,10 +353,7 @@ fn search_toolbar(
|
|||||||
">",
|
">",
|
||||||
IF!(pager.has_next_page => ev(Ev::Click, |_| Msg::NextPage))
|
IF!(pager.has_next_page => ev(Ev::Click, |_| Msg::NextPage))
|
||||||
],
|
],
|
||||||
ul![
|
ul![C!["pagination-list"], li![format!("{count} results")],],
|
||||||
C!["NOTPORTED", "pagination-list"],
|
|
||||||
li![format!("{count} results")],
|
|
||||||
],
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -494,11 +368,7 @@ fn raw_text_message(contents: &str) -> Node<Msg> {
|
|||||||
} else {
|
} else {
|
||||||
(contents, None)
|
(contents, None)
|
||||||
};
|
};
|
||||||
div![
|
div![C!["view-part-text-plain"], contents, truncated_msg,]
|
||||||
C!["NOTPORTED", "view-part-text-plain"],
|
|
||||||
contents,
|
|
||||||
truncated_msg,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_unread(tags: &[String]) -> bool {
|
fn has_unread(tags: &[String]) -> bool {
|
||||||
@ -548,7 +418,7 @@ fn render_avatar(photo_url: Option<String>, from: &str, big: bool) -> Node<Msg>
|
|||||||
fn copy_text_widget(text: &str) -> Node<Msg> {
|
fn copy_text_widget(text: &str) -> Node<Msg> {
|
||||||
let text = text.to_string();
|
let text = text.to_string();
|
||||||
span![
|
span![
|
||||||
i![C!["NOTPORTED", "far", "fa-clone"]],
|
i![C!["far", "fa-clone"]],
|
||||||
ev(Ev::Click, move |e| {
|
ev(Ev::Click, move |e| {
|
||||||
e.stop_propagation();
|
e.stop_propagation();
|
||||||
Msg::CopyToClipboard(text)
|
Msg::CopyToClipboard(text)
|
||||||
@ -746,9 +616,9 @@ fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod
|
|||||||
fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool) -> Node<Msg> {
|
fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool) -> Node<Msg> {
|
||||||
let expand_id = msg.id.clone();
|
let expand_id = msg.id.clone();
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "message", "bg-grey-900"],
|
C!["message", "bg-grey-900"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "header"],
|
C!["header"],
|
||||||
if open {
|
if open {
|
||||||
render_open_header(&msg)
|
render_open_header(&msg)
|
||||||
} else {
|
} else {
|
||||||
@ -765,13 +635,13 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
|
|||||||
],
|
],
|
||||||
IF!(open =>
|
IF!(open =>
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED","body", "mail"],
|
C!["body", "mail"],
|
||||||
match &msg.body {
|
match &msg.body {
|
||||||
ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType(
|
ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType(
|
||||||
ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree},
|
ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree},
|
||||||
) => div![
|
) => div![
|
||||||
raw_text_message(&contents),
|
raw_text_message(&contents),
|
||||||
div![C!["NOTPORTED","error"],
|
div![C!["error"],
|
||||||
view_content_tree(&content_tree),
|
view_content_tree(&content_tree),
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
@ -790,14 +660,14 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
|
|||||||
content_tree,
|
content_tree,
|
||||||
},
|
},
|
||||||
) => div![
|
) => div![
|
||||||
C!["NOTPORTED","view-part-text-html"],
|
C!["view-part-text-html"],
|
||||||
raw![contents],
|
raw![contents],
|
||||||
IF!(!msg.attachments.is_empty() =>
|
IF!(!msg.attachments.is_empty() =>
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED","attachments"],
|
C!["attachments"],
|
||||||
hr![],
|
hr![],
|
||||||
h2!["NOTPORTED","Attachments"],
|
h2!["Attachments"],
|
||||||
div![C!["NOTPORTED","grid","is-col-min-6"],
|
div![C!["grid","is-col-min-6"],
|
||||||
msg.attachments
|
msg.attachments
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| {
|
.map(|a| {
|
||||||
@ -810,12 +680,12 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
|
|||||||
fmtr.with_scales(Scales::Binary());
|
fmtr.with_scales(Scales::Binary());
|
||||||
|
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED","attachment", "card"],
|
C!["attachment", "card"],
|
||||||
a.content_type.as_ref().map(|content_type|
|
a.content_type.as_ref().map(|content_type|
|
||||||
IF!(content_type.starts_with("image/") =>
|
IF!(content_type.starts_with("image/") =>
|
||||||
div![C!["NOTPORTED","card-image","is-1by1"],
|
div![C!["card-image","is-1by1"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED","image","is-1by1"],
|
C!["image","is-1by1"],
|
||||||
style!{
|
style!{
|
||||||
St::BackgroundImage=>format!(r#"url("{url}");"#),
|
St::BackgroundImage=>format!(r#"url("{url}");"#),
|
||||||
St::BackgroundSize=>"cover",
|
St::BackgroundSize=>"cover",
|
||||||
@ -824,15 +694,15 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool)
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
)),
|
)),
|
||||||
div![C!["NOTPORTED","card-content"],
|
div![C!["card-content"],
|
||||||
div![C!["NOTPORTED","content"],
|
div![C!["content"],
|
||||||
&a.filename, br![],
|
&a.filename, br![],
|
||||||
small![ fmtr.format(a.size as f64),"B"]
|
small![ fmtr.format(a.size as f64),"B"]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
footer![
|
footer![
|
||||||
C!["NOTPORTED","card-footer"],
|
C!["card-footer"],
|
||||||
a![C!["NOTPORTED","card-footer-item"],span![C!["NOTPORTED","icon"], i![C!["NOTPORTED","fas", "fa-download"]]],
|
a![C!["card-footer-item"],span![C!["icon"], i![C!["fas", "fa-download"]]],
|
||||||
ev(Ev::Click, move |_| {
|
ev(Ev::Click, move |_| {
|
||||||
seed::window().location().set_href(&url
|
seed::window().location().set_href(&url
|
||||||
).expect("failed to set URL");
|
).expect("failed to set URL");
|
||||||
@ -883,25 +753,22 @@ fn thread(
|
|||||||
let spam_add_thread_id = thread.thread_id.clone();
|
let spam_add_thread_id = thread.thread_id.clone();
|
||||||
let spam_unread_thread_id = thread.thread_id.clone();
|
let spam_unread_thread_id = thread.thread_id.clone();
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "thread"],
|
C!["thread"],
|
||||||
h3![C!["NOTPORTED", "is-size-5"], subject],
|
h3![C!["is-size-5"], subject],
|
||||||
span![
|
span![
|
||||||
C!["NOTPORTED", "tags"],
|
C!["tags"],
|
||||||
removable_tags_chiclet(&thread.thread_id, &tags, false)
|
removable_tags_chiclet(&thread.thread_id, &tags, false)
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "level", "is-mobile"],
|
C!["level", "is-mobile"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "level-item"],
|
C!["level-item"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "buttons", "has-addons"],
|
C!["buttons", "has-addons"],
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED", "button", "mark-read"],
|
C!["button", "mark-read"],
|
||||||
attrs! {At::Title => "Mark as read"},
|
attrs! {At::Title => "Mark as read"},
|
||||||
span![
|
span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
|
||||||
C!["NOTPORTED", "icon", "is-small"],
|
|
||||||
i![C!["NOTPORTED", "far", "fa-envelope-open"]]
|
|
||||||
],
|
|
||||||
IF!(show_icon_text=>span!["Read"]),
|
IF!(show_icon_text=>span!["Read"]),
|
||||||
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
||||||
Msg::SetUnread(read_thread_id, false),
|
Msg::SetUnread(read_thread_id, false),
|
||||||
@ -909,12 +776,9 @@ fn thread(
|
|||||||
])),
|
])),
|
||||||
],
|
],
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED", "button", "mark-unread"],
|
C!["button", "mark-unread"],
|
||||||
attrs! {At::Title => "Mark as unread"},
|
attrs! {At::Title => "Mark as unread"},
|
||||||
span![
|
span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
|
||||||
C!["NOTPORTED", "icon", "is-small"],
|
|
||||||
i![C!["NOTPORTED", "far", "fa-envelope"]]
|
|
||||||
],
|
|
||||||
IF!(show_icon_text=>span!["Unread"]),
|
IF!(show_icon_text=>span!["Unread"]),
|
||||||
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
||||||
Msg::SetUnread(unread_thread_id, true),
|
Msg::SetUnread(unread_thread_id, true),
|
||||||
@ -924,16 +788,13 @@ fn thread(
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "level-item"],
|
C!["level-item"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "buttons", "has-addons"],
|
C!["buttons", "has-addons"],
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED", "button", "spam"],
|
C!["button", "spam"],
|
||||||
attrs! {At::Title => "Spam"},
|
attrs! {At::Title => "Spam"},
|
||||||
span![
|
span![C!["icon", "is-small"], i![C!["far", "fa-hand"]]],
|
||||||
C!["NOTPORTED", "icon", "is-small"],
|
|
||||||
i![C!["NOTPORTED", "far", "fa-hand"]]
|
|
||||||
],
|
|
||||||
IF!(show_icon_text=>span!["Spam"]),
|
IF!(show_icon_text=>span!["Spam"]),
|
||||||
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
||||||
Msg::AddTag(spam_add_thread_id, "Spam".to_string()),
|
Msg::AddTag(spam_add_thread_id, "Spam".to_string()),
|
||||||
@ -973,7 +834,7 @@ fn view_content_tree(content_tree: &str) -> Node<Msg> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
IF!(debug_open.get() =>
|
IF!(debug_open.get() =>
|
||||||
pre![C!["NOTPORTED","content-tree"], content_tree]),
|
pre![C!["content-tree"], content_tree]),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -987,17 +848,12 @@ fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
|
|||||||
};
|
};
|
||||||
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
||||||
nav![
|
nav![
|
||||||
C!["NOTPORTED", "navbar"],
|
C!["navbar"],
|
||||||
attrs! {At::Role=>"navigation"},
|
attrs! {At::Role=>"navigation"},
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "navbar-start"],
|
C!["navbar-start"],
|
||||||
a![
|
a![
|
||||||
C![
|
C!["navbar-item", "button", IF![is_error => "is-danger"]],
|
||||||
"NOTPORTED",
|
|
||||||
"navbar-item",
|
|
||||||
"button",
|
|
||||||
IF![is_error => "is-danger"]
|
|
||||||
],
|
|
||||||
span![i![C![
|
span![i![C![
|
||||||
"fa-solid",
|
"fa-solid",
|
||||||
"fa-arrow-rotate-right",
|
"fa-arrow-rotate-right",
|
||||||
@ -1007,21 +863,21 @@ fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
|
|||||||
ev(Ev::Click, |_| Msg::RefreshStart),
|
ev(Ev::Click, |_| Msg::RefreshStart),
|
||||||
],
|
],
|
||||||
a![
|
a![
|
||||||
C!["NOTPORTED", "navbar-item", "button"],
|
C!["navbar-item", "button"],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Href => urls::search(unread_query(), 0)
|
At::Href => urls::search(unread_query(), 0)
|
||||||
},
|
},
|
||||||
"Unread",
|
"Unread",
|
||||||
],
|
],
|
||||||
a![
|
a![
|
||||||
C!["NOTPORTED", "navbar-item", "button"],
|
C!["navbar-item", "button"],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Href => urls::search("", 0)
|
At::Href => urls::search("", 0)
|
||||||
},
|
},
|
||||||
"All",
|
"All",
|
||||||
],
|
],
|
||||||
input![
|
input![
|
||||||
C!["NOTPORTED", "navbar-item", "input"],
|
C!["navbar-item", "input"],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Placeholder => "Search";
|
At::Placeholder => "Search";
|
||||||
At::AutoFocus => true.as_at_value();
|
At::AutoFocus => true.as_at_value();
|
||||||
@ -1039,6 +895,25 @@ fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `view` describes what to display.
|
||||||
|
pub fn view(model: &Model) -> Node<Msg> {
|
||||||
|
let win = seed::window();
|
||||||
|
let w = win
|
||||||
|
.inner_width()
|
||||||
|
.expect("window width")
|
||||||
|
.as_f64()
|
||||||
|
.expect("window width f64");
|
||||||
|
let _h = win
|
||||||
|
.inner_height()
|
||||||
|
.expect("window height")
|
||||||
|
.as_f64()
|
||||||
|
.expect("window height f64");
|
||||||
|
div![match w {
|
||||||
|
w if w < 800. => div![C!["mobile"], mobile::view(model)],
|
||||||
|
w if w < 1024. => div![C!["tablet"], tablet::view(model)],
|
||||||
|
_ => div![C!["desktop"], desktop::view(model)],
|
||||||
|
},]
|
||||||
|
}
|
||||||
pub fn tags(model: &Model) -> Node<Msg> {
|
pub fn tags(model: &Model) -> Node<Msg> {
|
||||||
fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> {
|
fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> {
|
||||||
let href = if search_unread {
|
let href = if search_unread {
|
||||||
@ -1050,9 +925,9 @@ pub fn tags(model: &Model) -> Node<Msg> {
|
|||||||
attrs! {
|
attrs! {
|
||||||
At::Href => href
|
At::Href => href
|
||||||
},
|
},
|
||||||
(0..indent).map(|_| span![C!["pl-4"], ""]),
|
(0..indent).map(|_| span![C!["tag-indent"], ""]),
|
||||||
i![
|
i![
|
||||||
C!["px-1", "fa-solid", "fa-tag"],
|
C!["tag-tag", "fa-solid", "fa-tag"],
|
||||||
style! {
|
style! {
|
||||||
//"--fa-primary-color" => t.fg_color,
|
//"--fa-primary-color" => t.fg_color,
|
||||||
St::Color => t.bg_color,
|
St::Color => t.bg_color,
|
||||||
@ -1081,7 +956,7 @@ pub fn tags(model: &Model) -> Node<Msg> {
|
|||||||
for t in tags {
|
for t in tags {
|
||||||
let parts: Vec<_> = t.name.split('/').collect();
|
let parts: Vec<_> = t.name.split('/').collect();
|
||||||
let mut n = matches(&last, &parts);
|
let mut n = matches(&last, &parts);
|
||||||
if n + 2 <= parts.len() && parts.len() > 1 {
|
if n <= parts.len() - 2 && parts.len() > 1 {
|
||||||
// Synthesize fake tags for proper indenting.
|
// Synthesize fake tags for proper indenting.
|
||||||
for i in n..parts.len() - 1 {
|
for i in n..parts.len() - 1 {
|
||||||
let display_name = parts[n];
|
let display_name = parts[n];
|
||||||
@ -1112,9 +987,11 @@ pub fn tags(model: &Model) -> Node<Msg> {
|
|||||||
let tags_open = use_state(|| false);
|
let tags_open = use_state(|| false);
|
||||||
let force_tags_open = unread.is_empty();
|
let force_tags_open = unread.is_empty();
|
||||||
aside![
|
aside![
|
||||||
IF!(!unread.is_empty() => p![ "Unread"]),
|
C!["tags-menu", "menu"],
|
||||||
IF!(!unread.is_empty() => ul![view_tag_list(unread.into_iter(), true)]),
|
IF!(!unread.is_empty() => p![C!["menu-label"], "Unread"]),
|
||||||
|
IF!(!unread.is_empty() => ul![C!["menu-list"], view_tag_list(unread.into_iter(),true)]),
|
||||||
p![
|
p![
|
||||||
|
C!["menu-label"],
|
||||||
IF!(!force_tags_open =>
|
IF!(!force_tags_open =>
|
||||||
i![C![
|
i![C![
|
||||||
"fa-solid",
|
"fa-solid",
|
||||||
@ -1130,6 +1007,7 @@ pub fn tags(model: &Model) -> Node<Msg> {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
ul![
|
ul![
|
||||||
|
C!["menu-list"],
|
||||||
IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tag_list(tags.iter(),false))),
|
IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tag_list(tags.iter(),false))),
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -1146,30 +1024,29 @@ fn news_post(
|
|||||||
fn tag(tag: String, is_mobile: bool) -> Node<Msg> {
|
fn tag(tag: String, is_mobile: bool) -> Node<Msg> {
|
||||||
let hex = compute_color(&tag);
|
let hex = compute_color(&tag);
|
||||||
let style = style! {St::BackgroundColor=>hex};
|
let style = style! {St::BackgroundColor=>hex};
|
||||||
let classes = C!["NOTPORTED", "tag", IF!(is_mobile => "is-small")];
|
let classes = C!["tag", IF!(is_mobile => "is-small")];
|
||||||
let attrs = attrs! {
|
let attrs = attrs! {
|
||||||
At::Href => urls::search(&format!("tag:{tag}"), 0)
|
At::Href => urls::search(&format!("tag:{tag}"), 0)
|
||||||
};
|
};
|
||||||
let tag = tag.clone();
|
let tag = tag.clone();
|
||||||
div![
|
div![
|
||||||
C![
|
C![
|
||||||
"NOTPORTED",
|
|
||||||
"message-tags",
|
"message-tags",
|
||||||
"field",
|
"field",
|
||||||
"is-grouped",
|
"is-grouped",
|
||||||
"is-grouped-multiline"
|
"is-grouped-multiline"
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "control"],
|
C!["control"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "tags", "has-addons"],
|
C!["tags", "has-addons"],
|
||||||
a![
|
a![
|
||||||
classes,
|
classes,
|
||||||
attrs,
|
attrs,
|
||||||
style,
|
style,
|
||||||
match tag.as_str() {
|
match tag.as_str() {
|
||||||
"attachment" => span!["📎"],
|
"attachment" => span!["📎"],
|
||||||
"replied" => span![i![C!["NOTPORTED", "fa-solid", "fa-reply"]]],
|
"replied" => span![i![C!["fa-solid", "fa-reply"]]],
|
||||||
_ => span![&tag],
|
_ => span![&tag],
|
||||||
},
|
},
|
||||||
ev(Ev::Click, move |_| Msg::FrontPageRequest {
|
ev(Ev::Click, move |_| Msg::FrontPageRequest {
|
||||||
@ -1186,22 +1063,19 @@ fn news_post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "thread"],
|
C!["thread"],
|
||||||
h3![C!["NOTPORTED", "is-size-5"], subject],
|
h3![C!["is-size-5"], subject],
|
||||||
tag(format!("News/{}", post.slug), false),
|
tag(format!("News/{}", post.slug), false),
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "level", "is-mobile"],
|
C!["level", "is-mobile"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "level-item"],
|
C!["level-item"],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "buttons", "has-addons"],
|
C!["buttons", "has-addons"],
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED", "button", "mark-read"],
|
C!["button", "mark-read"],
|
||||||
attrs! {At::Title => "Mark as read"},
|
attrs! {At::Title => "Mark as read"},
|
||||||
span![
|
span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
|
||||||
C!["NOTPORTED", "icon", "is-small"],
|
|
||||||
i![C!["NOTPORTED", "far", "fa-envelope-open"]]
|
|
||||||
],
|
|
||||||
IF!(show_icon_text=>span!["Read"]),
|
IF!(show_icon_text=>span!["Read"]),
|
||||||
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
||||||
Msg::SetUnread(read_thread_id, false),
|
Msg::SetUnread(read_thread_id, false),
|
||||||
@ -1209,12 +1083,9 @@ fn news_post(
|
|||||||
])),
|
])),
|
||||||
],
|
],
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED", "button", "mark-unread"],
|
C!["button", "mark-unread"],
|
||||||
attrs! {At::Title => "Mark as unread"},
|
attrs! {At::Title => "Mark as unread"},
|
||||||
span![
|
span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
|
||||||
C!["NOTPORTED", "icon", "is-small"],
|
|
||||||
i![C!["NOTPORTED", "far", "fa-envelope"]]
|
|
||||||
],
|
|
||||||
IF!(show_icon_text=>span!["Unread"]),
|
IF!(show_icon_text=>span!["Unread"]),
|
||||||
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
ev(Ev::Click, move |_| Msg::MultiMsg(vec![
|
||||||
Msg::SetUnread(unread_thread_id, true),
|
Msg::SetUnread(unread_thread_id, true),
|
||||||
@ -1225,18 +1096,13 @@ fn news_post(
|
|||||||
],
|
],
|
||||||
// This would be the holder for spam buttons on emails, needed to keep layout
|
// This would be the holder for spam buttons on emails, needed to keep layout
|
||||||
// consistent
|
// consistent
|
||||||
div![C!["NOTPORTED", "level-item"], div![]]
|
div![C!["level-item"], div![]]
|
||||||
],
|
],
|
||||||
div![
|
div![
|
||||||
C!["NOTPORTED", "message"],
|
C!["message"],
|
||||||
div![C!["NOTPORTED", "header"], render_news_post_header(&post)],
|
div![C!["header"], render_news_post_header(&post)],
|
||||||
div![
|
div![
|
||||||
C![
|
C!["body", "news-post", format!("site-{}", post.slug)],
|
||||||
"NOTPORTED",
|
|
||||||
"body",
|
|
||||||
"news-post",
|
|
||||||
format!("site-{}", post.slug)
|
|
||||||
],
|
|
||||||
el_ref(content_el),
|
el_ref(content_el),
|
||||||
raw![&post.body]
|
raw![&post.body]
|
||||||
]
|
]
|
||||||
@ -1320,10 +1186,14 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node<Msg>
|
|||||||
}
|
}
|
||||||
fn reading_progress(ratio: f64) -> Node<Msg> {
|
fn reading_progress(ratio: f64) -> Node<Msg> {
|
||||||
let percent = ratio * 100.;
|
let percent = ratio * 100.;
|
||||||
// TODO: percent broken with no styles
|
|
||||||
info!("percent {percent}");
|
|
||||||
progress![
|
progress![
|
||||||
C!["absolute", "w-screen", IF!(percent<1. => "hidden")],
|
C![
|
||||||
|
"read-progress",
|
||||||
|
"progress",
|
||||||
|
"is-success",
|
||||||
|
"is-small",
|
||||||
|
IF!(percent<1. => "is-invisible")
|
||||||
|
],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Value=>percent,
|
At::Value=>percent,
|
||||||
At::Max=>"100"
|
At::Max=>"100"
|
||||||
@ -1334,29 +1204,26 @@ fn reading_progress(ratio: f64) -> Node<Msg> {
|
|||||||
pub fn versions(versions: &crate::state::Version) -> Node<Msg> {
|
pub fn versions(versions: &crate::state::Version) -> Node<Msg> {
|
||||||
debug!("versions {versions:?}");
|
debug!("versions {versions:?}");
|
||||||
aside![
|
aside![
|
||||||
C!["NOTPORTED", "tags-menu", "menu"],
|
C!["tags-menu", "menu"],
|
||||||
p![C!["NOTPORTED", "menu-label"], "Versions"],
|
p![C!["menu-label"], "Versions"],
|
||||||
ul![
|
ul![
|
||||||
C!["NOTPORTED", "menu-list"],
|
C!["menu-list"],
|
||||||
li!["Client"],
|
li!["Client"],
|
||||||
li![span![C!["NOTPORTED", "tag-indent"], &versions.client]]
|
li![span![C!["tag-indent"], &versions.client]]
|
||||||
],
|
],
|
||||||
versions.server.as_ref().map(|v| ul![
|
versions.server.as_ref().map(|v| ul![
|
||||||
C!["NOTPORTED", "menu-list"],
|
C!["menu-list"],
|
||||||
li!["Server"],
|
li!["Server"],
|
||||||
li![span![C!["NOTPORTED", "tag-indent"], v]]
|
li![span![C!["tag-indent"], v]]
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn click_to_top() -> Node<Msg> {
|
fn click_to_top() -> Node<Msg> {
|
||||||
button![
|
button![
|
||||||
C!["NOTPORTED", "button", "is-danger", "is-small"],
|
C!["button", "is-danger", "is-small"],
|
||||||
span!["Top"],
|
span!["Top"],
|
||||||
span![
|
span![C!["icon"], i![C!["fas", "fa-arrow-turn-up"]]],
|
||||||
C!["NOTPORTED", "icon"],
|
|
||||||
i![C!["NOTPORTED", "fas", "fa-arrow-turn-up"]]
|
|
||||||
],
|
|
||||||
ev(Ev::Click, |_| web_sys::window()
|
ev(Ev::Click, |_| web_sys::window()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.scroll_to_with_x_and_y(0., 0.))
|
.scroll_to_with_x_and_y(0., 0.))
|
||||||
|
|||||||
48
web/src/view/tablet.rs
Normal file
48
web/src/view/tablet.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
268
web/static/main.css
Normal file
268
web/static/main.css
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user