Merge branch 'customization' into 'main'
Light & Dark Mode See merge request vade/vade-mecum!5
This commit is contained in:
commit
62bec56241
|
@ -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",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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<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 {
|
||||
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)
|
||||
|
|
|
@ -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 {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
Self {
|
||||
|
|
17
src/main.js
17
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')
|
||||
|
|
48
src/main.rs
48
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<HashMap<i32, bool>>,
|
||||
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<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:.*}")]
|
||||
async fn dist(path: web::Path<String>) -> HttpResponse {
|
||||
let path = if UIAssets::get(&*path).is_some() {
|
||||
|
@ -172,12 +205,13 @@ async fn dist(path: web::Path<String>) -> HttpResponse {
|
|||
#[instrument]
|
||||
async fn setup_database(db_path: &str) -> error::Result<DatabaseConnection> {
|
||||
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?)
|
||||
|
|
|
@ -13,7 +13,6 @@ body {
|
|||
height: 100%;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 35px;
|
||||
background-color: #222;
|
||||
background-repeat: none;
|
||||
background-size: cover;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<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 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 Bookmark" v-if="editMode" @click="openNewBookmark" />
|
||||
<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 class="container" v-if="appsForCategory(null).length > 0 || editMode">
|
||||
<h1>APPLICATIONS</h1>
|
||||
|
@ -111,7 +114,7 @@ export default {
|
|||
},
|
||||
connect_ws() {
|
||||
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) => {
|
||||
const data = JSON.parse(event.data);
|
||||
const { app_id, alive } = data.HealthcheckChange;
|
||||
|
@ -158,6 +161,9 @@ export default {
|
|||
this.editBookmarkCat = {};
|
||||
this.bookmarkCatOpen = false;
|
||||
},
|
||||
settingsClick() {
|
||||
this.$router.push("/settings");
|
||||
},
|
||||
toggleEdit() {
|
||||
this.editMode = !this.editMode;
|
||||
},
|
||||
|
@ -208,6 +214,10 @@ export default {
|
|||
this.editBookmarkCat = {};
|
||||
this.bookmarkCatOpen = false;
|
||||
this.bookmarkOpen = false;
|
||||
|
||||
this.themeMode = (await axios.get("/api/settings/theme/mode")).data.mode || "dark";
|
||||
localStorage.setItem("themeMode", this.themeMode);
|
||||
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
@ -220,6 +230,13 @@ export default {
|
|||
BookmarkTile,
|
||||
BookmarkCategoryModal,
|
||||
},
|
||||
computed: {
|
||||
dashboardStyle() {
|
||||
return {
|
||||
backgroundImage: `url('${this.$rootUrl()}/bg.jpg')`
|
||||
};
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editMode: false,
|
||||
|
@ -236,6 +253,7 @@ export default {
|
|||
bookmarks: [],
|
||||
bookmarkCategories: [],
|
||||
connection: null,
|
||||
themeMode: localStorage.getItem("themeMode"),
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
|
@ -245,7 +263,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
<style scoped>
|
||||
h1 {
|
||||
margin-top: 0px;
|
||||
font-weight: bold;
|
||||
|
@ -259,6 +277,11 @@ h1 {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.lightMode h1 {
|
||||
color: rgba(0,0,0,0.5);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
text-transform: uppercase;
|
||||
|
@ -268,6 +291,12 @@ h2 {
|
|||
color: rgba(255,255,255,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 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -275,6 +304,7 @@ h2 {
|
|||
h2 svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.app-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 25%);
|
||||
|
@ -288,15 +318,21 @@ h2 svg {
|
|||
background-color: rgba(0,0,0,0.4);
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.lightMode .container {
|
||||
background-color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.container svg {
|
||||
color: rgba(255,255,255,0.25);
|
||||
}
|
||||
.lightMode .container svg {
|
||||
color: rgba(0,0,0,0.25);
|
||||
}
|
||||
.dashboard {
|
||||
user-select: none;
|
||||
min-height: calc(100vh - 150px);
|
||||
min-width: 100vw;
|
||||
padding-top: 150px;
|
||||
background: url('/bg.jpg');
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
@ -308,6 +344,7 @@ h2 svg {
|
|||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bookmark-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
@ -322,4 +359,32 @@ h2 svg {
|
|||
bottom: 20px;
|
||||
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>
|
||||
|
|
|
@ -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