Light & Dark Mode
This commit is contained in:
parent
b1c8342253
commit
82704aaa35
|
@ -203,6 +203,24 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
|
@ -1132,9 +1150,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4"
|
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -1147,9 +1165,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b"
|
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
@ -1157,15 +1175,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7"
|
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a"
|
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
@ -1185,15 +1203,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2"
|
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c"
|
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1202,21 +1220,21 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508"
|
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72"
|
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.19"
|
version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164"
|
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -3328,12 +3346,28 @@ version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
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]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unchecked-index"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
|
@ -3410,6 +3444,7 @@ version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
|
"actix-multipart",
|
||||||
"actix-rt 2.6.0",
|
"actix-rt 2.6.0",
|
||||||
"actix-web 4.0.0-rc.2",
|
"actix-web 4.0.0-rc.2",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
|
@ -3418,6 +3453,7 @@ dependencies = [
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"futures",
|
||||||
"jemallocator",
|
"jemallocator",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
|
|
|
@ -31,6 +31,8 @@ base64 = "0.13.0"
|
||||||
sqlx = { version = "^0.5", features=["sqlite", "migrate"] }
|
sqlx = { version = "^0.5", features=["sqlite", "migrate"] }
|
||||||
reqwest = { version = "0.11.9", features = ["rustls-tls"], default-features=false }
|
reqwest = { version = "0.11.9", features = ["rustls-tls"], default-features=false }
|
||||||
clap = { version = "3.0.14", features=["cargo", "env"] }
|
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]
|
[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator]
|
||||||
|
|
|
@ -3,4 +3,4 @@ COPY target/x86_64-unknown-linux-musl/release/vade /app/vade
|
||||||
EXPOSE 8089
|
EXPOSE 8089
|
||||||
WORKDIR app
|
WORKDIR app
|
||||||
RUN mkdir data
|
RUN mkdir data
|
||||||
CMD ["./vade", "--db data/"]
|
CMD ./vade --db data/
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
use actix_web::web::Data;
|
||||||
use actix_web::Scope;
|
use actix_web::Scope;
|
||||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{api, auth};
|
use crate::{api, auth, AppState};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -64,6 +65,7 @@ pub mod applications;
|
||||||
pub mod authorization;
|
pub mod authorization;
|
||||||
pub mod bookmark_categories;
|
pub mod bookmark_categories;
|
||||||
pub mod bookmarks;
|
pub mod bookmarks;
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
mod api_prelude {
|
mod api_prelude {
|
||||||
pub use super::ListObjects;
|
pub use super::ListObjects;
|
||||||
|
@ -107,6 +109,7 @@ pub mod test_prelude {
|
||||||
Ok(actix_web::web::Data::new(AppState {
|
Ok(actix_web::web::Data::new(AppState {
|
||||||
db,
|
db,
|
||||||
healthcheck_status: Mutex::new(HashMap::new()),
|
healthcheck_status: Mutex::new(HashMap::new()),
|
||||||
|
data_path: ".".to_string(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,6 +130,29 @@ impl<T: Serialize> ListObjects<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<AppState>,
|
||||||
|
) -> crate::error::Result<HttpResponse> {
|
||||||
|
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 {
|
pub fn routes() -> Scope {
|
||||||
let auth_handler = HttpAuthentication::bearer(auth::validator);
|
let auth_handler = HttpAuthentication::bearer(auth::validator);
|
||||||
let protected_routes = Scope::new("")
|
let protected_routes = Scope::new("")
|
||||||
|
@ -135,7 +161,9 @@ pub fn routes() -> Scope {
|
||||||
.service(api::application_categories::routes())
|
.service(api::application_categories::routes())
|
||||||
.service(api::bookmarks::routes())
|
.service(api::bookmarks::routes())
|
||||||
.service(api::bookmark_categories::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")
|
Scope::new("api")
|
||||||
.service(api::authorization::authorize)
|
.service(api::authorization::authorize)
|
||||||
|
|
|
@ -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<AppState>) -> crate::error::Result<HttpResponse> {
|
||||||
|
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<ThemeModeRequest>,
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
) -> crate::error::Result<HttpResponse> {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -77,6 +77,15 @@ impl From<sea_orm::DbErr> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<actix_web::error::BlockingError> for Error {
|
||||||
|
fn from(source: actix_web::error::BlockingError) -> Self {
|
||||||
|
Self {
|
||||||
|
code: ErrorCode::Internal,
|
||||||
|
message: source.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<sqlx::Error> for Error {
|
impl From<sqlx::Error> for Error {
|
||||||
fn from(e: sqlx::Error) -> Self {
|
fn from(e: sqlx::Error) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
15
src/main.js
15
src/main.js
|
@ -13,10 +13,14 @@ axios.interceptors.request.use(function (config) {
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function rootUrl() {
|
||||||
axios.defaults.baseURL = process.env.NODE_ENV === 'development'
|
return process.env.NODE_ENV === 'development'
|
||||||
? 'http://localhost:8089'
|
? 'http://localhost:8089'
|
||||||
: '';
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
axios.defaults.baseURL = rootUrl();
|
||||||
|
|
||||||
axios.interceptors.response.use(function (response) {
|
axios.interceptors.response.use(function (response) {
|
||||||
return response;
|
return response;
|
||||||
|
@ -34,8 +38,15 @@ axios.interceptors.response.use(function (response) {
|
||||||
|
|
||||||
library.add(fas)
|
library.add(fas)
|
||||||
|
|
||||||
|
const rootUrlPlugin = {
|
||||||
|
install: (app) => {
|
||||||
|
app.config.globalProperties.$rootUrl = rootUrl
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
createApp(App)
|
createApp(App)
|
||||||
.use(router)
|
.use(router)
|
||||||
.component("font-awesome-icon", FontAwesomeIcon)
|
.component("font-awesome-icon", FontAwesomeIcon)
|
||||||
|
.use(rootUrlPlugin)
|
||||||
.mount('#app')
|
.mount('#app')
|
||||||
|
|
48
src/main.rs
48
src/main.rs
|
@ -1,7 +1,11 @@
|
||||||
use std::{collections::HashMap, path::Path};
|
use std::{collections::HashMap, path::Path};
|
||||||
|
|
||||||
use actix::SystemService;
|
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 clap::crate_version;
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use sea_orm::{prelude::*, Database};
|
use sea_orm::{prelude::*, Database};
|
||||||
|
@ -24,6 +28,7 @@ static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db: DatabaseConnection,
|
pub db: DatabaseConnection,
|
||||||
pub healthcheck_status: Mutex<HashMap<i32, bool>>,
|
pub healthcheck_status: Mutex<HashMap<i32, bool>>,
|
||||||
|
pub data_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
|
@ -41,9 +46,9 @@ async fn main() {
|
||||||
.takes_value(true),
|
.takes_value(true),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
clap::Arg::new("db")
|
clap::Arg::new("data")
|
||||||
.env("VADE_DB")
|
.env("VADE_DB")
|
||||||
.long("db")
|
.long("data")
|
||||||
.value_name("path")
|
.value_name("path")
|
||||||
.default_value("./")
|
.default_value("./")
|
||||||
.help("Sets the path to the database location")
|
.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");
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let state = web::Data::new(AppState {
|
let state = web::Data::new(AppState {
|
||||||
db,
|
db,
|
||||||
healthcheck_status: Mutex::new(HashMap::new()),
|
healthcheck_status: Mutex::new(HashMap::new()),
|
||||||
|
data_path: opts.value_of("data").unwrap_or_default().to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
|
@ -137,6 +143,7 @@ async fn main() {
|
||||||
.wrap(cors)
|
.wrap(cors)
|
||||||
.app_data(state.clone())
|
.app_data(state.clone())
|
||||||
.wrap(tracing_actix_web::TracingLogger::default())
|
.wrap(tracing_actix_web::TracingLogger::default())
|
||||||
|
.service(get_bg)
|
||||||
.service(web::resource("/events/{token}").to(socket_session::event_session_index))
|
.service(web::resource("/events/{token}").to(socket_session::event_session_index))
|
||||||
.service(api::routes())
|
.service(api::routes())
|
||||||
.service(dist)
|
.service(dist)
|
||||||
|
@ -152,6 +159,32 @@ async fn main() {
|
||||||
#[folder = "dist"]
|
#[folder = "dist"]
|
||||||
struct UIAssets;
|
struct UIAssets;
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
fn read_file(path: &Path) -> error::Result<Vec<u8>> {
|
||||||
|
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<AppState>) -> error::Result<HttpResponse> {
|
||||||
|
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:.*}")]
|
#[get("/{filename:.*}")]
|
||||||
async fn dist(path: web::Path<String>) -> HttpResponse {
|
async fn dist(path: web::Path<String>) -> HttpResponse {
|
||||||
let path = if UIAssets::get(&*path).is_some() {
|
let path = if UIAssets::get(&*path).is_some() {
|
||||||
|
@ -172,12 +205,13 @@ async fn dist(path: web::Path<String>) -> HttpResponse {
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn setup_database(db_path: &str) -> error::Result<DatabaseConnection> {
|
async fn setup_database(db_path: &str) -> error::Result<DatabaseConnection> {
|
||||||
let db_fname = "data.db";
|
let db_fname = "data.db";
|
||||||
|
let full_path = Path::new(db_path).join(db_fname);
|
||||||
if !Path::new(db_path).join(db_fname).exists() {
|
if !full_path.exists() {
|
||||||
std::fs::File::create(db_fname)?;
|
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?;
|
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||||
tracing::info!("Database migrated");
|
tracing::info!("Database migrated");
|
||||||
Ok(Database::connect("sqlite://data.db").await?)
|
Ok(Database::connect("sqlite://data.db").await?)
|
||||||
|
|
|
@ -13,7 +13,6 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: "Roboto", sans-serif;
|
||||||
font-size: 35px;
|
font-size: 35px;
|
||||||
background-color: #222;
|
|
||||||
background-repeat: none;
|
background-repeat: none;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,10 @@ svg {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode svg {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
.tile.alive svg {
|
.tile.alive svg {
|
||||||
color: #009900;
|
color: #009900;
|
||||||
}
|
}
|
||||||
|
@ -62,4 +66,16 @@ svg {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.lightMode .title {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.lightMode .description {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode .tile:hover {
|
||||||
|
background-color: rgba(200,200,200, 0.5);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -28,6 +28,9 @@ export default {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
.lightMode .bookmark-tile, .lightMode .bookmark-tile svg {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
.bookmark-tile:hover, .bookmark-tile:hover svg {
|
.bookmark-tile:hover, .bookmark-tile:hover svg {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
@ -35,5 +38,10 @@ export default {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode .bookmark-tile:hover, .lightMode .bookmark-tile:hover svg {
|
||||||
|
background: rgba(255,255,255,0.25);
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -17,4 +17,7 @@ export default {
|
||||||
.tile.new {
|
.tile.new {
|
||||||
background: rgba(0,0,0,0.8);
|
background: rgba(0,0,0,0.8);
|
||||||
}
|
}
|
||||||
|
.lightMode .tile.new {
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,6 +2,11 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Dashboard from "../views/Dashboard.vue"
|
import Dashboard from "../views/Dashboard.vue"
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
component: () => import(/* webpackChunkName: "login" */ '../views/Settings.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/logout',
|
path: '/logout',
|
||||||
name: 'Logout',
|
name: 'Logout',
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{dashboard: true, 'edit-mode': editMode}">
|
<div :class="{dashboard: true, 'edit-mode': editMode, lightMode: this.themeMode == 'light'}" :style="dashboardStyle">
|
||||||
<div @click="vade_click" class="vade">vade</div>
|
<div @click="vade_click" class="vade">vade</div>
|
||||||
<div class="editmode-tiles">
|
<div class="editmode-tiles">
|
||||||
<new-item-tile title="New Application" v-if="editMode" @click="openNewApp" />
|
<new-item-tile title="New App" v-if="editMode" @click="openNewApp" />
|
||||||
<new-item-tile title="New App Category" v-if="editMode" @click="openNewAppCat" />
|
<new-item-tile title="New App Category" v-if="editMode" @click="openNewAppCat" />
|
||||||
<new-item-tile title="New Bookmark" v-if="editMode" @click="openNewBookmark" />
|
<new-item-tile title="New Bookmark" v-if="editMode" @click="openNewBookmark" />
|
||||||
<new-item-tile title="New Bookmark Category" v-if="editMode" @click="openNewBookmarkCat" />
|
<new-item-tile title="New Bookmark Category" v-if="editMode" @click="openNewBookmarkCat" />
|
||||||
|
<div class="settings-tile" v-if="editMode" @click="settingsClick">
|
||||||
|
<font-awesome-icon icon="cog" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container" v-if="appsForCategory(null).length > 0 || editMode">
|
<div class="container" v-if="appsForCategory(null).length > 0 || editMode">
|
||||||
<h1>APPLICATIONS</h1>
|
<h1>APPLICATIONS</h1>
|
||||||
|
@ -111,7 +114,7 @@ export default {
|
||||||
},
|
},
|
||||||
connect_ws() {
|
connect_ws() {
|
||||||
const token = localStorage.getItem("token")
|
const token = localStorage.getItem("token")
|
||||||
this.connection = new WebSocket("ws://localhost:8088/events/" + encodeURIComponent(token));
|
this.connection = new WebSocket("ws://localhost:8089/events/" + encodeURIComponent(token));
|
||||||
this.connection.onmessage = (event) => {
|
this.connection.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
const { app_id, alive } = data.HealthcheckChange;
|
const { app_id, alive } = data.HealthcheckChange;
|
||||||
|
@ -158,6 +161,9 @@ export default {
|
||||||
this.editBookmarkCat = {};
|
this.editBookmarkCat = {};
|
||||||
this.bookmarkCatOpen = false;
|
this.bookmarkCatOpen = false;
|
||||||
},
|
},
|
||||||
|
settingsClick() {
|
||||||
|
this.$router.push("/settings");
|
||||||
|
},
|
||||||
toggleEdit() {
|
toggleEdit() {
|
||||||
this.editMode = !this.editMode;
|
this.editMode = !this.editMode;
|
||||||
},
|
},
|
||||||
|
@ -208,6 +214,10 @@ export default {
|
||||||
this.editBookmarkCat = {};
|
this.editBookmarkCat = {};
|
||||||
this.bookmarkCatOpen = false;
|
this.bookmarkCatOpen = false;
|
||||||
this.bookmarkOpen = false;
|
this.bookmarkOpen = false;
|
||||||
|
|
||||||
|
this.themeMode = (await axios.get("/api/settings/theme/mode")).data.mode || "dark";
|
||||||
|
localStorage.setItem("themeMode", this.themeMode);
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -220,6 +230,13 @@ export default {
|
||||||
BookmarkTile,
|
BookmarkTile,
|
||||||
BookmarkCategoryModal,
|
BookmarkCategoryModal,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
dashboardStyle() {
|
||||||
|
return {
|
||||||
|
backgroundImage: `url('${this.$rootUrl()}/bg.jpg')`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editMode: false,
|
editMode: false,
|
||||||
|
@ -236,6 +253,7 @@ export default {
|
||||||
bookmarks: [],
|
bookmarks: [],
|
||||||
bookmarkCategories: [],
|
bookmarkCategories: [],
|
||||||
connection: null,
|
connection: null,
|
||||||
|
themeMode: localStorage.getItem("themeMode"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
@ -245,7 +263,7 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scope>
|
<style scoped>
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -259,6 +277,11 @@ h1 {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode h1 {
|
||||||
|
color: rgba(0,0,0,0.5);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
@ -268,6 +291,12 @@ h2 {
|
||||||
color: rgba(255,255,255,0.25);
|
color: rgba(255,255,255,0.25);
|
||||||
text-shadow: 1px 1px rgba(0,0,0,0.25);
|
text-shadow: 1px 1px rgba(0,0,0,0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode h2 {
|
||||||
|
color: rgba(0,0,0,0.5);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-mode .editable {
|
.edit-mode .editable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -275,6 +304,7 @@ h2 {
|
||||||
h2 svg {
|
h2 svg {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-tiles {
|
.app-tiles {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 25%);
|
grid-template-columns: repeat(4, 25%);
|
||||||
|
@ -288,15 +318,21 @@ h2 svg {
|
||||||
background-color: rgba(0,0,0,0.4);
|
background-color: rgba(0,0,0,0.4);
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode .container {
|
||||||
|
background-color: rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
.container svg {
|
.container svg {
|
||||||
color: rgba(255,255,255,0.25);
|
color: rgba(255,255,255,0.25);
|
||||||
}
|
}
|
||||||
|
.lightMode .container svg {
|
||||||
|
color: rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
.dashboard {
|
.dashboard {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
min-height: calc(100vh - 150px);
|
min-height: calc(100vh - 150px);
|
||||||
min-width: 100vw;
|
min-width: 100vw;
|
||||||
padding-top: 150px;
|
padding-top: 150px;
|
||||||
background: url('/bg.jpg');
|
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
@ -308,6 +344,7 @@ h2 svg {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookmark-container {
|
.bookmark-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
@ -322,4 +359,32 @@ h2 svg {
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
left: 40px;
|
left: 40px;
|
||||||
}
|
}
|
||||||
|
.settings-tile {
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
transition: all 0.5s;
|
||||||
|
padding: 16px 25px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.settings-tile:hover {
|
||||||
|
transition: all 0.1s;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.5) 0px 20px 30px -10px;
|
||||||
|
}
|
||||||
|
.settings-tile svg {
|
||||||
|
color: #fff;
|
||||||
|
margin: auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode .settings-tile {
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode .settings-tile svg {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<div class="settings">
|
||||||
|
<div class="section">
|
||||||
|
<h1>Wallpaper</h1>
|
||||||
|
<h2>Upload a background wallpaper</h2>
|
||||||
|
<input ref="bg_file" type="file"/>
|
||||||
|
<div class="actions">
|
||||||
|
<btn label="Update" @click="uploadBg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<h1>Theme</h1>
|
||||||
|
<h2>Select light or dark mode to match the wallpaper</h2>
|
||||||
|
<switch-field v-model="darkMode" label="Darkmode" />
|
||||||
|
<div class="actions">
|
||||||
|
<btn label="Update" @click="updateMode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<btn primary label="Back to Dashboard" @click="backClicked" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Btn from "../components/Button.vue";
|
||||||
|
import SwitchField from "../components/Switch.vue";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Btn, SwitchField },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
file: "",
|
||||||
|
darkMode: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.darkMode = ((await axios.get("/api/settings/theme/mode")).data.mode || "dark") === "dark";
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async uploadBg() {
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append("file", this.$refs.bg_file.files[0]);
|
||||||
|
await axios.post("/api/upload/bg", formData, {
|
||||||
|
headers: {'content-type': 'multipart/form-data'}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateMode() {
|
||||||
|
await axios.put("/api/settings/theme/mode", {
|
||||||
|
mode: this.darkMode ? "dark" : "light"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
backClicked() {
|
||||||
|
this.$router.push("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.settings {
|
||||||
|
padding-top: 100px;
|
||||||
|
min-height: calc(100vh - 100px);
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
width: 800px;
|
||||||
|
margin: auto auto;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #121212;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
.section + .section {
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
.section h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.control-buttons {
|
||||||
|
width: 800px;
|
||||||
|
margin: auto auto;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue