From 82704aaa354e5154c05c9e8a0a0eb7700ce82a2f Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 15 Feb 2022 00:46:49 +0000 Subject: [PATCH] Light & Dark Mode --- Cargo.lock | 72 ++++++++++++++----- Cargo.toml | 2 + Dockerfile | 2 +- src/api/mod.rs | 32 ++++++++- src/api/settings.rs | 63 +++++++++++++++++ src/error.rs | 9 +++ src/main.js | 17 ++++- src/main.rs | 48 +++++++++++-- src/ui/App.vue | 1 - src/ui/components/ApplicationTile.vue | 16 +++++ src/ui/components/BookmarkTile.vue | 8 +++ src/ui/components/NewItemTile.vue | 3 + src/ui/router/index.js | 5 ++ src/ui/views/Dashboard.vue | 75 ++++++++++++++++++-- src/ui/views/Settings.vue | 99 +++++++++++++++++++++++++++ 15 files changed, 415 insertions(+), 37 deletions(-) create mode 100644 src/api/settings.rs create mode 100644 src/ui/views/Settings.vue diff --git a/Cargo.lock b/Cargo.lock index e82d945..18a5b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -203,6 +203,24 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-multipart" +version = "0.4.0-beta.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59b1f14a8b2bc14df9be544d173f5390da5b62d531e406fd0f0ce9b825fea5a" +dependencies = [ + "actix-utils 3.0.0", + "actix-web 4.0.0-rc.2", + "bytes 1.1.0", + "derive_more", + "futures-core", + "httparse", + "local-waker", + "log", + "mime", + "twoway", +] + [[package]] name = "actix-router" version = "0.2.7" @@ -1132,9 +1150,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -1147,9 +1165,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -1157,15 +1175,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -1185,15 +1203,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", @@ -1202,21 +1220,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", @@ -3328,12 +3346,28 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "twoway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47" +dependencies = [ + "memchr", + "unchecked-index", +] + [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + [[package]] name = "unicase" version = "2.6.0" @@ -3410,6 +3444,7 @@ version = "0.1.1" dependencies = [ "actix", "actix-cors", + "actix-multipart", "actix-rt 2.6.0", "actix-web 4.0.0-rc.2", "actix-web-actors", @@ -3418,6 +3453,7 @@ dependencies = [ "bcrypt", "chrono", "clap", + "futures", "jemallocator", "jsonwebtoken", "mime_guess", diff --git a/Cargo.toml b/Cargo.toml index 5f0b02b..f1bc9a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,8 @@ base64 = "0.13.0" sqlx = { version = "^0.5", features=["sqlite", "migrate"] } reqwest = { version = "0.11.9", features = ["rustls-tls"], default-features=false } clap = { version = "3.0.14", features=["cargo", "env"] } +actix-multipart = "0.4.0-beta.13" +futures = "0.3.21" [target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator] diff --git a/Dockerfile b/Dockerfile index 6585cde..23eb195 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,4 +3,4 @@ COPY target/x86_64-unknown-linux-musl/release/vade /app/vade EXPOSE 8089 WORKDIR app RUN mkdir data -CMD ["./vade", "--db data/"] +CMD ./vade --db data/ diff --git a/src/api/mod.rs b/src/api/mod.rs index 8a6b1bd..d9fd565 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,8 +1,9 @@ +use actix_web::web::Data; use actix_web::Scope; use actix_web_httpauth::middleware::HttpAuthentication; use serde::{Deserialize, Serialize}; -use crate::{api, auth}; +use crate::{api, auth, AppState}; #[macro_export] #[cfg(test)] @@ -64,6 +65,7 @@ pub mod applications; pub mod authorization; pub mod bookmark_categories; pub mod bookmarks; +pub mod settings; mod api_prelude { pub use super::ListObjects; @@ -107,6 +109,7 @@ pub mod test_prelude { Ok(actix_web::web::Data::new(AppState { db, healthcheck_status: Mutex::new(HashMap::new()), + data_path: ".".to_string(), })) } } @@ -127,6 +130,29 @@ impl ListObjects { } } +use actix_multipart::Multipart; +use actix_web::{post, web, HttpResponse}; +use futures::StreamExt; +use std::io::Write; +use std::path::Path; + +#[post("/upload/bg")] +async fn bg_upload( + mut payload: Multipart, + state: Data, +) -> crate::error::Result { + if let Some(Ok(mut field)) = payload.next().await { + let file_path = Path::new(&state.data_path).join("bg.jpg"); + let mut f = web::block(|| std::fs::File::create(file_path)).await??; + while let Some(chunk) = field.next().await { + let data = chunk.unwrap(); + f = web::block(move || f.write_all(&data).map(|_| f)).await??; + } + } + + Ok(HttpResponse::Ok().body("")) +} + pub fn routes() -> Scope { let auth_handler = HttpAuthentication::bearer(auth::validator); let protected_routes = Scope::new("") @@ -135,7 +161,9 @@ pub fn routes() -> Scope { .service(api::application_categories::routes()) .service(api::bookmarks::routes()) .service(api::bookmark_categories::routes()) - .service(api::authorization::update_password); + .service(api::settings::routes()) + .service(api::authorization::update_password) + .service(bg_upload); Scope::new("api") .service(api::authorization::authorize) diff --git a/src/api/settings.rs b/src/api/settings.rs new file mode 100644 index 0000000..94bc921 --- /dev/null +++ b/src/api/settings.rs @@ -0,0 +1,63 @@ +use crate::entity::setting; +use actix_web::{web, HttpResponse}; +use serde::{Deserialize, Serialize}; + +use super::api_prelude::*; + +#[derive(Debug, Serialize, Deserialize)] +struct ThemeModeResponse { + mode: String, +} + +#[get("theme/mode")] +async fn theme_mode(state: web::Data) -> crate::error::Result { + let mode = setting::Entity::find() + .filter(setting::Column::SettingName.eq("theme_mode".to_string())) + .one(&state.db) + .await? + .map(|r| r.setting_value) + .unwrap_or_else(|| "dark".to_string()); + + Ok(HttpResponse::Ok().json(ThemeModeResponse { mode })) +} + +#[derive(Debug, Serialize, Deserialize)] +struct ThemeModeRequest { + mode: String, +} + +#[put("theme/mode")] +async fn update_theme_mode( + data: web::Json, + state: web::Data, +) -> crate::error::Result { + let ThemeModeRequest { mode } = data.0; + let setting = setting::Entity::find() + .filter(setting::Column::SettingName.eq("theme_mode".to_string())) + .one(&state.db) + .await?; + + if let Some(setting) = setting { + let rec = setting::ActiveModel { + id: Set(setting.id), + setting_value: Set(mode.clone()), + ..Default::default() + }; + rec.update(&state.db).await?; + } else { + let rec = setting::ActiveModel { + setting_name: Set("theme_mode".to_string()), + setting_value: Set(mode.clone()), + ..Default::default() + }; + rec.save(&state.db).await?; + } + + Ok(HttpResponse::Ok().json(ThemeModeResponse { mode })) +} + +pub fn routes() -> Scope { + web::scope("/settings") + .service(theme_mode) + .service(update_theme_mode) +} diff --git a/src/error.rs b/src/error.rs index 008bb95..16c461b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -77,6 +77,15 @@ impl From for Error { } } +impl From for Error { + fn from(source: actix_web::error::BlockingError) -> Self { + Self { + code: ErrorCode::Internal, + message: source.to_string(), + } + } +} + impl From for Error { fn from(e: sqlx::Error) -> Self { Self { diff --git a/src/main.js b/src/main.js index c5816bc..8da0688 100644 --- a/src/main.js +++ b/src/main.js @@ -13,10 +13,14 @@ axios.interceptors.request.use(function (config) { return config; }); +function rootUrl() { + return process.env.NODE_ENV === 'development' + ? 'http://localhost:8089' + : ''; +} -axios.defaults.baseURL = process.env.NODE_ENV === 'development' - ? 'http://localhost:8089' - : ''; + +axios.defaults.baseURL = rootUrl(); axios.interceptors.response.use(function (response) { return response; @@ -34,8 +38,15 @@ axios.interceptors.response.use(function (response) { library.add(fas) +const rootUrlPlugin = { + install: (app) => { + app.config.globalProperties.$rootUrl = rootUrl + } +}; + createApp(App) .use(router) .component("font-awesome-icon", FontAwesomeIcon) + .use(rootUrlPlugin) .mount('#app') diff --git a/src/main.rs b/src/main.rs index a8f747e..d8fa96d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ use std::{collections::HashMap, path::Path}; use actix::SystemService; -use actix_web::{get, web, App, HttpResponse, HttpServer}; +use actix_web::{ + get, + web::{self, Data}, + App, HttpResponse, HttpServer, +}; use clap::crate_version; use rust_embed::RustEmbed; use sea_orm::{prelude::*, Database}; @@ -24,6 +28,7 @@ static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; pub struct AppState { pub db: DatabaseConnection, pub healthcheck_status: Mutex>, + pub data_path: String, } #[actix_rt::main] @@ -41,9 +46,9 @@ async fn main() { .takes_value(true), ) .arg( - clap::Arg::new("db") + clap::Arg::new("data") .env("VADE_DB") - .long("db") + .long("data") .value_name("path") .default_value("./") .help("Sets the path to the database location") @@ -60,12 +65,13 @@ async fn main() { ); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector"); - let db = setup_database(opts.value_of("db").unwrap_or_default()) + let db = setup_database(opts.value_of("data").unwrap_or_default()) .await .unwrap(); let state = web::Data::new(AppState { db, healthcheck_status: Mutex::new(HashMap::new()), + data_path: opts.value_of("data").unwrap_or_default().to_string(), }); info!( @@ -137,6 +143,7 @@ async fn main() { .wrap(cors) .app_data(state.clone()) .wrap(tracing_actix_web::TracingLogger::default()) + .service(get_bg) .service(web::resource("/events/{token}").to(socket_session::event_session_index)) .service(api::routes()) .service(dist) @@ -152,6 +159,32 @@ async fn main() { #[folder = "dist"] struct UIAssets; +#[instrument] +fn read_file(path: &Path) -> error::Result> { + let f = std::fs::File::open(path)?; + let mut reader = std::io::BufReader::new(f); + let mut buffer = Vec::new(); + std::io::Read::read_to_end(&mut reader, &mut buffer)?; + tracing::info!("Read into buffer: {}", buffer.len()); + Ok(buffer) +} + +#[get("/bg.jpg")] +async fn get_bg(state: Data) -> error::Result { + let file_path = Path::new(&state.data_path).join("bg.jpg"); + if file_path.exists() { + let data = read_file(&file_path).unwrap(); + Ok(HttpResponse::Ok().content_type("image/jpg").body(data)) + } else { + let content = UIAssets::get("bg.jpg").unwrap(); + let body: actix_web::body::BoxBody = match content { + std::borrow::Cow::Borrowed(bytes) => actix_web::body::BoxBody::new(bytes), + std::borrow::Cow::Owned(bytes) => actix_web::body::BoxBody::new(bytes), + }; + Ok(HttpResponse::Ok().content_type("image/jpg").body(body)) + } +} + #[get("/{filename:.*}")] async fn dist(path: web::Path) -> HttpResponse { let path = if UIAssets::get(&*path).is_some() { @@ -172,12 +205,13 @@ async fn dist(path: web::Path) -> HttpResponse { #[instrument] async fn setup_database(db_path: &str) -> error::Result { let db_fname = "data.db"; - - if !Path::new(db_path).join(db_fname).exists() { + let full_path = Path::new(db_path).join(db_fname); + if !full_path.exists() { std::fs::File::create(db_fname)?; } - let pool = sqlx::SqlitePool::connect("sqlite://data.db").await?; + let pool = + sqlx::SqlitePool::connect(&format!("sqlite://{}", full_path.to_str().unwrap())).await?; sqlx::migrate!("./migrations").run(&pool).await?; tracing::info!("Database migrated"); Ok(Database::connect("sqlite://data.db").await?) diff --git a/src/ui/App.vue b/src/ui/App.vue index 8bd8d5f..2ee7414 100644 --- a/src/ui/App.vue +++ b/src/ui/App.vue @@ -13,7 +13,6 @@ body { height: 100%; font-family: "Roboto", sans-serif; font-size: 35px; - background-color: #222; background-repeat: none; background-size: cover; } diff --git a/src/ui/components/ApplicationTile.vue b/src/ui/components/ApplicationTile.vue index db2718a..82cefc0 100644 --- a/src/ui/components/ApplicationTile.vue +++ b/src/ui/components/ApplicationTile.vue @@ -45,6 +45,10 @@ svg { margin-right: 20px; } +.lightMode svg { + color: #444; +} + .tile.alive svg { color: #009900; } @@ -62,4 +66,16 @@ svg { font-size: 0.8em; line-height: 1; } + + +.lightMode .title { + color: #000; +} + .lightMode .description { + color: #444; +} + +.lightMode .tile:hover { + background-color: rgba(200,200,200, 0.5); +} diff --git a/src/ui/components/BookmarkTile.vue b/src/ui/components/BookmarkTile.vue index 83072d9..4ac1aaa 100644 --- a/src/ui/components/BookmarkTile.vue +++ b/src/ui/components/BookmarkTile.vue @@ -28,6 +28,9 @@ export default { font-size: 16px; color: #ccc; } +.lightMode .bookmark-tile, .lightMode .bookmark-tile svg { + color: #222; + } .bookmark-tile:hover, .bookmark-tile:hover svg { transition: all 0.2s; @@ -35,5 +38,10 @@ export default { color: #fff; } +.lightMode .bookmark-tile:hover, .lightMode .bookmark-tile:hover svg { + background: rgba(255,255,255,0.25); + color: #222; +} + diff --git a/src/ui/components/NewItemTile.vue b/src/ui/components/NewItemTile.vue index e7a7901..2972baf 100644 --- a/src/ui/components/NewItemTile.vue +++ b/src/ui/components/NewItemTile.vue @@ -17,4 +17,7 @@ export default { .tile.new { background: rgba(0,0,0,0.8); } +.lightMode .tile.new { + background: rgba(255,255,255,0.8); +} diff --git a/src/ui/router/index.js b/src/ui/router/index.js index 7f6853b..9b2c111 100644 --- a/src/ui/router/index.js +++ b/src/ui/router/index.js @@ -2,6 +2,11 @@ import { createRouter, createWebHistory } from 'vue-router' import Dashboard from "../views/Dashboard.vue" const routes = [ + { + path: '/settings', + name: 'Settings', + component: () => import(/* webpackChunkName: "login" */ '../views/Settings.vue') + }, { path: '/logout', name: 'Logout', diff --git a/src/ui/views/Dashboard.vue b/src/ui/views/Dashboard.vue index d4f00c4..43f353d 100644 --- a/src/ui/views/Dashboard.vue +++ b/src/ui/views/Dashboard.vue @@ -1,11 +1,14 @@