diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7a79b75..33dabfa 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -24,7 +24,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.2", features = ["v4", "serde"] } [dev-dependencies] -lettre = { version = "0.10", default-features = false, features=["smtp-transport", "hostname", "builder"] } +lettre = { version = "0.10", default-features = false, features=[ + "builder", + "hostname", + "smtp-transport" +] } fake = { version = "2.5", features=["derive"]} reqwest = { version = "0.11", features = ["json"] } local-ip-address = "0.5" diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index ee3f902..8a90815 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -17,15 +17,16 @@ timeago = "0.4" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3", features = [ - "Event", - "EventTarget", - "NodeList", - "HtmlLinkElement", - "HtmlIFrameElement", "CssStyleDeclaration", "DomTokenList", "Element", + "Event", + "EventTarget", "HtmlElement", + "HtmlIFrameElement", + "HtmlLinkElement", + "MediaQueryList", + "NodeList" ] } yew = "0.20" yew-hooks = "0.2" diff --git a/frontend/img/dark-mode.svg b/frontend/img/dark-mode.svg index 95c6a1f..1f0d3f3 100644 --- a/frontend/img/dark-mode.svg +++ b/frontend/img/dark-mode.svg @@ -1,3 +1,3 @@ - - + + diff --git a/frontend/img/envelope-open.svg b/frontend/img/envelope-open.svg index 38bdae9..c94ce12 100644 --- a/frontend/img/envelope-open.svg +++ b/frontend/img/envelope-open.svg @@ -1,13 +1,13 @@ - - - - \ No newline at end of file + + + + diff --git a/frontend/img/envelope.svg b/frontend/img/envelope.svg index 3e58b2a..053eb7f 100644 --- a/frontend/img/envelope.svg +++ b/frontend/img/envelope.svg @@ -1,15 +1,15 @@ - - - - \ No newline at end of file + + + + diff --git a/frontend/img/trash.svg b/frontend/img/trash.svg index 57c173f..04ae922 100644 --- a/frontend/img/trash.svg +++ b/frontend/img/trash.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/src/dark_mode.rs b/frontend/src/dark_mode.rs new file mode 100644 index 0000000..ddc3edd --- /dev/null +++ b/frontend/src/dark_mode.rs @@ -0,0 +1,76 @@ +const DARK: &str = "dark"; +const LIGHT: &str = "light"; +const DARK_MODE_KEY: &str = "dark-mode"; + +const BODY_INVERT_KEY: &str = "body-invert"; +const INVERT: &str = "invert"; +const NO_INVERT: &str = "no-invert"; + +pub fn init_dark_mode() { + // fetch dark mode setting with media query, override with local storage + let local_storage = web_sys::window().unwrap().local_storage().unwrap().unwrap(); + let dark_mode = match local_storage.get_item(DARK_MODE_KEY).unwrap().as_deref() { + Some(DARK) => true, + Some(LIGHT) => false, + _ => { + web_sys::window() + .unwrap() + .match_media("(prefers-color-scheme: dark)") + .unwrap() + .map(|l| l.matches()) + == Some(true) + } + }; + + let body_invert = match local_storage.get_item(BODY_INVERT_KEY).unwrap().as_deref() { + Some(INVERT) => true, + _ => false, + }; + + let body = web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap(); + body.class_list() + .add_1(if dark_mode { DARK } else { LIGHT }) + .unwrap(); + body.class_list() + .add_1(if body_invert { INVERT } else { NO_INVERT }) + .unwrap(); +} + +pub fn toggle_dark_mode() { + let body = web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap(); + let dark_mode = body.class_list().contains(DARK); + let new_mode = if dark_mode { LIGHT } else { DARK }; + + body.class_list().remove_2(DARK, LIGHT).unwrap(); + body.class_list().add_1(new_mode).unwrap(); + + let local_storage = web_sys::window().unwrap().local_storage().unwrap().unwrap(); + local_storage.set_item(DARK_MODE_KEY, new_mode).unwrap() +} + +pub fn toggle_body_invert() { + let body = web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap(); + let body_invert = body.class_list().contains(INVERT); + let new_mode = if body_invert { NO_INVERT } else { INVERT }; + + body.class_list().remove_2(NO_INVERT, INVERT).unwrap(); + body.class_list().add_1(new_mode).unwrap(); + + let local_storage = web_sys::window().unwrap().local_storage().unwrap().unwrap(); + local_storage.set_item(BODY_INVERT_KEY, new_mode).unwrap() +} diff --git a/frontend/src/main.rs b/frontend/src/main.rs index ac7628b..bd44bda 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -1,6 +1,7 @@ use overview::Overview; mod api; +mod dark_mode; mod formatted; mod list; mod message_header; diff --git a/frontend/src/message_header.rs b/frontend/src/message_header.rs index de9231f..4bd787d 100644 --- a/frontend/src/message_header.rs +++ b/frontend/src/message_header.rs @@ -1,5 +1,5 @@ -use crate::types::MailMessage; -use yew::{function_component, html, html_nested, Html, Properties}; +use crate::{dark_mode::toggle_body_invert, types::MailMessage}; +use yew::{function_component, html, html_nested, Callback, Html, Properties}; #[derive(Properties, Eq, PartialEq)] pub struct MessageHeaderProps { @@ -68,7 +68,7 @@ pub fn view(props: &MessageHeaderProps) -> Html { -
+
{message.attachments.iter().map(|a| { html! { Html { } }).collect::()} +
} diff --git a/frontend/src/overview.rs b/frontend/src/overview.rs index 2bb2d4d..a9b4f88 100644 --- a/frontend/src/overview.rs +++ b/frontend/src/overview.rs @@ -5,6 +5,7 @@ use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use crate::api::fetch_messages_metadata; +use crate::dark_mode::{init_dark_mode, toggle_dark_mode}; use crate::list::MessageList; use crate::types::{Action, MailMessageMetadata}; use crate::view::ViewMessage; @@ -58,6 +59,10 @@ impl Component for Overview { } }); + spawn_local(async { + init_dark_mode(); + }); + Self { messages: vec![], tab: Tab::Formatted, @@ -129,17 +134,14 @@ impl Component for Overview {

{"Mail"}{"Crab"}

- if !self.messages.is_empty() { } +
if self.messages.is_empty() { diff --git a/frontend/style.scss b/frontend/style.scss index 3851b32..1935469 100644 --- a/frontend/style.scss +++ b/frontend/style.scss @@ -63,42 +63,60 @@ select { src: url('fonts/Inter-italic.var.woff2') format('woff2'); } -$grey: #ccc; -$border-grey: #eee; -$light: #f6f6f6; -$red: #f74c00; -$light-red: #fffafa; -$foreground: #333; -$background: white; - -$foreground-dark: #ffffff; -$light-red-dark: rgb(23, 22, 22); -$background-dark: rgb(10, 10, 10); -$surface-dark: rgb(25, 25, 25); -$border-grey-dark: rgb(75, 75, 75); +:root { + --black: black; + --white: white; + --grey: #ccc; + --table-border: #e6e6e6; + --border-grey: #eee; + --light: #f6f6f6; + --tab-background: #e9e9e9; + --red: #f74c00; + --light-red: #fffafa; + --foreground: #333; + --background: white; +} + +body { + &.dark { + --black: white; + --white: black; + --table-border: #3a3a3a; + --grey: #4b4b4b; + --border-grey: #242424; + --light: #202020; + --tab-background: #2c2c2c; + --light-red: #171616; + --foreground: white; + --background: #0a0a0a; + + .invert-body { + display: block !important; + } + + &.invert { + .body { + filter: invert(1); + } + } + } +} html, body { height: 100%; max-height: 100vh; - background: $background; - - @media (prefers-color-scheme: dark) { - background: $background-dark; - } + background: var(--background); + color: var(--foreground); } header { height: 4rem; - background: $light-red; + background: var(--light-red); display: flex; justify-content: space-between; align-items: center; - @media (prefers-color-scheme: dark) { - background: $light-red-dark; - } - h1 { line-height: 4rem; padding-left: 6.5rem; @@ -106,48 +124,38 @@ header { background-size: 5rem; font-weight: 300; font-size: 2.5rem; - color: $foreground; - - @media (prefers-color-scheme: dark) { - color: $foreground-dark; - } + color: var(--foreground); span { - color: $red; + color: var(--red); font-weight: 400; } } + div { + display: flex; + margin-right: 0.5rem; + } + button { - margin-right: 1rem; + display: inline-block; + margin-right: 0.5rem; padding: 0 1rem 0 1.9rem; - border: 1px solid $border-grey; + border: 1px solid var(--border-grey); cursor: pointer; border-radius: 0; font-weight: 400; font-size: 0.9rem; height: 2.5rem; line-height: 2rem; - background: white url('img/trash.svg') no-repeat top 0.6rem left 0.75rem; + background: var(--background) url('img/trash.svg') no-repeat top 0.6rem left + 0.75rem; background-size: 14px; - .invert-body { - background: white url('img/dark-mode.svg') no-repeat top 0.6rem left - 0.75rem; - padding: 0 1rem; - @media (prefers-color-scheme: light) { - display: none; - } - } - @media (prefers-color-scheme: dark) { - background: $surface-dark url('img/trash.svg') no-repeat top 0.6rem left - 0.75rem; - background-size: 14px; - color: $foreground-dark; - border: 1px solid $border-grey-dark; - .invert-body { - background: $surface-dark url('img/dark-mode.svg') no-repeat top 0.6rem - left 0.75rem; - } + color: var(--foreground); + + &.dark-mode { + background: var(--background) url('img/dark-mode.svg') no-repeat top + 0.7rem left 0.95rem; } span { @@ -157,7 +165,7 @@ header { } &:hover { - border-color: $red; + border-color: var(--red); } } } @@ -173,7 +181,7 @@ header { text-align: center; font-size: 3rem; font-weight: 200; - color: $grey; + color: var(--grey); } @keyframes slide-down { @@ -189,17 +197,13 @@ header { } .list { - border-right: 1px solid $border-grey; + border-right: 1px solid var(--border-grey); width: 30%; height: 100%; min-width: 40rem; max-width: 50rem; overflow-y: auto; - @media (prefers-color-scheme: dark) { - border-right: 1px solid $border-grey-dark; - } - ul { display: flex; flex-direction: column-reverse; @@ -207,11 +211,7 @@ header { li { padding: 0.75rem 0.75rem 0.75rem 3.5rem; - border-bottom: 1px solid $border-grey; - - @media (prefers-color-scheme: dark) { - border-bottom: 1px solid $border-grey-dark; - } + border-bottom: 1px solid var(--border-grey); line-height: 1.6; cursor: pointer; @@ -219,33 +219,18 @@ header { animation: slide-down 300ms ease-in; overflow: hidden; font-size: 0.9rem; - background: $background url('img/envelope.svg') no-repeat center left + background: var(--background) url('img/envelope.svg') no-repeat center left 0.75rem; background-size: 2rem; - color: $foreground; - - @media (prefers-color-scheme: dark) { - background: $surface-dark url('img/envelope.svg') no-repeat center left - 0.75rem; - background-size: 2rem; - color: $foreground-dark; - } + color: var(--foreground); &:last-child { - border-top: 1px solid $border-grey; - - @media (prefers-color-scheme: dark) { - border-top: 1px solid $border-grey-dark; - } + border-top: 1px solid var(--border-grey); } &:hover, &.selected { - background-color: $light-red; - - @media (prefers-color-scheme: dark) { - background-color: $light-red-dark; - } + background-color: var(--light-red); } &.opened { @@ -254,13 +239,9 @@ header { } &.selected { - color: $foreground !important; + color: var(--foreground) !important; font-weight: 500; background-image: url('img/envelope-open-text.svg'); - - @media (prefers-color-scheme: dark) { - color: $foreground-dark !important; - } } .head { @@ -273,11 +254,8 @@ header { .name + .email { margin-left: 0.5rem; - color: rgba(black, 0.3); - - @media (prefers-color-scheme: dark) { - color: $foreground-dark !important; - } + color: var(--black); + opacity: 0.4; &::before { content: '<'; @@ -291,20 +269,14 @@ header { .recipients { .label { - color: rgba(black, 0.6); - - @media (prefers-color-scheme: dark) { - color: rgba(white, 0.6); - } + color: var(--black); + opacity: 0.6; } .email, .etc { - color: rgba(black, 0.3); - - @media (prefers-color-scheme: dark) { - color: rgba(white, 1); - } + color: var(--black); + opacity: 0.4; } } @@ -313,22 +285,16 @@ header { justify-content: space-between; .subject { - color: rgba(black, 0.9); - - @media (prefers-color-scheme: dark) { - color: rgba(white, 0.9); - } + color: var(--black); + opacity: 0.9; display: block; } .size { margin-left: 0.5rem; - color: rgba(black, 0.3); - - @media (prefers-color-scheme: dark) { - color: rgba(white, 0.6); - } + color: var(--black); + opacity: 0.4; } } @@ -349,66 +315,43 @@ header { & > .email + .email:before { content: ', <'; display: inline-block; - color: rgba(black, 0.3); - - @media (prefers-color-scheme: dark) { - color: $foreground-dark !important; - } + color: var(--black); + opacity: 0.4; } .email { &::before { content: '<'; - color: rgba(black, 0.3); - - @media (prefers-color-scheme: dark) { - color: $foreground-dark !important; - } + color: var(--black); + opacity: 0.4; } &::after { content: '>'; - color: rgba(black, 0.3); - - @media (prefers-color-scheme: dark) { - color: $foreground-dark !important; - } + color: var(--black); + opacity: 0.4; } } } .view { - background-color: $light; + background-color: var(--light); padding: 1rem; flex: 1; overflow-x: hidden; - border-top: 1px solid $border-grey; - - @media (prefers-color-scheme: dark) { - background-color: $background-dark; - border-top: 1px solid $border-grey-dark; - } + border-top: 1px solid var(--border-grey); .view-inner { - background-color: white; + background-color: var(--white); height: 100%; display: flex; flex-direction: column; - @media (prefers-color-scheme: dark) { - background-color: $surface-dark; - color: $foreground-dark; - } - ul { display: block; - background-color: $light; + background-color: var(--light); display: flex; - @media (prefers-color-scheme: dark) { - background-color: $background-dark; - } - li { button { font-size: 1rem; @@ -416,21 +359,13 @@ header { margin-right: 0.3rem; cursor: pointer; border: none; - background-color: darken($light, 5%); - - @media (prefers-color-scheme: dark) { - background-color: lighten($background-dark, 5%); - color: $foreground-dark; - } + background-color: var(--tab-background); + color: var(--foreground); &:hover, &.active { - background: white; + background: var(--white); transition: all 150ms ease-in; - - @media (prefers-color-scheme: dark) { - background: $surface-dark; - } } } @@ -441,20 +376,13 @@ header { margin-right: 0; margin-bottom: 0.25rem; padding: 0.5rem 1.5rem 0.5rem 2rem; - background: white url('img/trash.svg') no-repeat center left 1rem; + background: var(--white) url('img/trash.svg') no-repeat center left + 1rem; background-size: 12px; - border: 1px solid $grey; - - @media (prefers-color-scheme: dark) { - background: $surface-dark url('img/trash.svg') no-repeat center - left 1rem; - background-size: 12px; - color: $foreground-dark; - border: 1px solid $border-grey-dark; - } + border: 1px solid var(--grey); &:hover { - border: 1px solid $red; + border: 1px solid var(--red); } } } @@ -481,11 +409,7 @@ header { td, th { padding: 0.5rem; - border-bottom: 1px solid lighten($grey, 10%); - - @media (prefers-color-scheme: dark) { - border-bottom: 1px solid lighten($background-dark, 25%); - } + border-bottom: 1px solid var(--table-border); } .name { @@ -493,11 +417,7 @@ header { } .name + .email { - color: rgba(black, 0.8); - - @media (prefers-color-scheme: dark) { - color: $foreground-dark !important; - } + color: rgba(var(--black), 0.8); &::before { content: ' <'; @@ -509,27 +429,21 @@ header { } } - .attachments { - a { + .actions { + a, + button { display: inline-block; padding: 0.5rem 0.5rem 0.5rem 2rem; margin: 0.75rem 0.5rem 0 0; font-size: 0.9rem; - border: 1px solid $grey; + border: 1px solid var(--grey); text-decoration: none; - color: black; + color: var(--black); transition: all 150ms ease-in; - background: white url('img/file.svg') no-repeat center left 0.5rem; + background: var(--white) url('img/file.svg') no-repeat center left + 0.5rem; background-size: 1rem; - @media (prefers-color-scheme: dark) { - background: $surface-dark url('img/file.svg') no-repeat center left - 0.5rem; - background-size: 1rem; - color: $foreground-dark; - border: 1px solid $border-grey-dark; - } - &.application-pdf { background-image: url('img/file-pdf.svg'); } @@ -537,17 +451,21 @@ header { .size { margin-left: 0.5rem; font-size: 0.8rem; - color: rgba(black, 0.7); + color: rgba(var(--black), 0.7); } &:hover { - background-color: $light-red; - - @media (prefers-color-scheme: dark) { - background-color: $light-red-dark; - } + background-color: var(--light-red); } } + + .invert-body { + float: right; + cursor: pointer; + display: none; + background-image: url('img/dark-mode.svg'); + margin-right: 0; + } } pre { @@ -572,7 +490,7 @@ header { iframe, pre { - border: 1px solid $border-grey; + border: 1px solid #ddd; width: 100%; height: 100%; padding: 0.5rem; @@ -582,12 +500,6 @@ header { } } -body.body-invert { - .body { - filter: invert(1); - } -} - .bouncing-loader { width: 100vw; height: 100vh; @@ -615,7 +527,7 @@ body.body-invert { width: 1rem; height: 1rem; margin: 3rem 0.25rem; - background: #f74c00; + background: var(--red); border-radius: 50%; animation: bouncing-loader 0.6s infinite alternate; }