From 45d356aa9248e8ffe57c6f519a13faf602a4f9ff Mon Sep 17 00:00:00 2001 From: Adrian Wozniak Date: Thu, 23 Apr 2020 21:29:50 +0200 Subject: [PATCH] Build css with rust. Fix letter avatar --- jirs-client/.gitignore | 1 + jirs-client/js/css/project.css | 10 +- jirs-client/js/css/styledAvatar.css | 32 +++ jirs-client/js/index.js | 2 +- jirs-client/src/project.rs | 10 +- jirs-client/src/shared/styled_avatar.rs | 53 ++++- jirs-client/webpack.config.js | 4 + jirs-css/src/main.rs | 294 +++++++++++++++++------- 8 files changed, 304 insertions(+), 102 deletions(-) diff --git a/jirs-client/.gitignore b/jirs-client/.gitignore index 6fc919c6..bf3ce765 100644 --- a/jirs-client/.gitignore +++ b/jirs-client/.gitignore @@ -2,3 +2,4 @@ pkg node_modules dist .yarn-error.log +tmp diff --git a/jirs-client/js/css/project.css b/jirs-client/js/css/project.css index 43972470..0b963f9a 100644 --- a/jirs-client/js/css/project.css +++ b/jirs-client/js/css/project.css @@ -148,12 +148,6 @@ width: 90px; } -@media (max-width: 1100px) { - #projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue { - padding: 10px 8px; - } -} - #projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue:hover { background: var(--backgroundLight); } @@ -164,6 +158,10 @@ } @media (max-width: 1100px) { + #projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue { + padding: 10px 8px; + } + #projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue > .title { font-size: 14.5px } diff --git a/jirs-client/js/css/styledAvatar.css b/jirs-client/js/css/styledAvatar.css index b959ecf6..541ad445 100644 --- a/jirs-client/js/css/styledAvatar.css +++ b/jirs-client/js/css/styledAvatar.css @@ -27,3 +27,35 @@ justify-content: center; height: 100%; } + +.styledAvatar.avatarColor1, .styledAvatar span.avatarColor1 { + color: var(--avatar-color-1); +} + +.styledAvatar.avatarColor2, .styledAvatar span.avatarColor2 { + color: var(--avatar-color-2); +} + +.styledAvatar.avatarColor3, .styledAvatar span.avatarColor3 { + color: var(--avatar-color-3); +} + +.styledAvatar.avatarColor4, .styledAvatar span.avatarColor4 { + color: var(--avatar-color-4); +} + +.styledAvatar.avatarColor5, .styledAvatar span.avatarColor5 { + color: var(--avatar-color-5); +} + +.styledAvatar.avatarColor6, .styledAvatar span.avatarColor6 { + color: var(--avatar-color-6); +} + +.styledAvatar.avatarColor7, .styledAvatar span.avatarColor7 { + color: var(--avatar-color-7); +} + +.styledAvatar.avatarColor8, .styledAvatar span.avatarColor8 { + color: var(--avatar-color-8); +} diff --git a/jirs-client/js/index.js b/jirs-client/js/index.js index e019a24e..05dacccd 100644 --- a/jirs-client/js/index.js +++ b/jirs-client/js/index.js @@ -1,4 +1,4 @@ -import "./styles.css"; +import "../tmp/styles.css"; const getWsHostName = () => process.env.JIRS_SERVER_BIND === "0.0.0.0" ? 'localhost' : process.env.JIRS_SERVER_BIND; const getProtocol = () => window.location.protocol.replace(/^http/, 'ws'); diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index 977da9cd..9c00925f 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -228,7 +228,8 @@ fn avatars_filters(model: &Model) -> Node { let avatars: Vec> = model .users .iter() - .map(|user| { + .enumerate() + .map(|(idx, user)| { let mut class_list = vec!["avatarIsActiveBorder"]; let user_id = user.id; let active = active_avatar_filters.contains(&user_id); @@ -241,6 +242,7 @@ fn avatars_filters(model: &Model) -> Node { Msg::ProjectAvatarFilterChanged(user_id, active) })) .name(user.name.as_str()) + .user_index(idx) .build() .into_node(); div![attrs![At::Class => class_list.join(" ")], styled_avatar] @@ -345,12 +347,14 @@ fn project_issue(model: &Model, issue: &Issue) -> Node { let avatars: Vec> = model .users .iter() - .filter(|user| issue.user_ids.contains(&user.id)) - .map(|user| { + .enumerate() + .filter(|(_, user)| issue.user_ids.contains(&user.id)) + .map(|(idx, user)| { StyledAvatar::build() .size(24) .name(user.name.as_str()) .avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default()) + .user_index(idx) .build() .into_node() }) diff --git a/jirs-client/src/shared/styled_avatar.rs b/jirs-client/src/shared/styled_avatar.rs index 2f0caf8e..a4bb7a62 100644 --- a/jirs-client/src/shared/styled_avatar.rs +++ b/jirs-client/src/shared/styled_avatar.rs @@ -9,6 +9,7 @@ pub struct StyledAvatar { name: String, on_click: Option>, class_list: Vec, + user_index: usize, } impl Default for StyledAvatar { @@ -19,6 +20,7 @@ impl Default for StyledAvatar { name: "".to_string(), on_click: None, class_list: vec![], + user_index: 0, } } } @@ -31,6 +33,7 @@ impl StyledAvatar { name: "".to_string(), on_click: None, class_list: vec![], + user_index: 0, } } } @@ -47,6 +50,7 @@ pub struct StyledAvatarBuilder { name: String, on_click: Option>, class_list: Vec, + user_index: usize, } impl StyledAvatarBuilder { @@ -87,6 +91,11 @@ impl StyledAvatarBuilder { self } + pub fn user_index(mut self, user_index: usize) -> Self { + self.user_index = user_index; + self + } + pub fn build(self) -> StyledAvatar { StyledAvatar { avatar_url: self.avatar_url, @@ -94,6 +103,7 @@ impl StyledAvatarBuilder { name: self.name, on_click: self.on_click, class_list: self.class_list, + user_index: self.user_index, } } } @@ -105,8 +115,11 @@ pub fn render(values: StyledAvatar) -> Node { name, on_click, mut class_list, + user_index, } = values; + let index = user_index % 8; + class_list.push("styledAvatar".to_string()); match avatar_url { Some(_) => class_list.push("image".to_string()), @@ -118,15 +131,37 @@ pub fn render(values: StyledAvatar) -> Node { None => vec![], Some(h) => vec![h], }; + let letter = name + .chars() + .rev() + .last() + .map(|c| c.to_string()) + .unwrap_or_default(); match avatar_url { - Some(url) => div![ - attrs![At::Class => class_list.join(" "), At::Style => format!("{shared}; background-image: url({url});", shared = shared_style, url = url)], - handler, - ], - _ => div![ - attrs![At::Class => class_list.join(" "), At::Style => shared_style], - span![name], - handler - ], + Some(url) => { + let style = format!( + "{shared}; background-image: url({url});", + shared = shared_style, + url = url + ); + div![ + attrs![At::Class => class_list.join(" "), At::Style => style], + handler, + ] + } + _ => { + let style = format!( + "{shared}; width: {size}px; height: {size}px; font-size: calc({size}px / 1.7);", + shared = shared_style, + size = size + ); + class_list.push("letter".to_string()); + class_list.push(format!("avatarColor{}", index + 1)); + div![ + attrs![At::Class => class_list.join(" "), At::Style => style], + span![letter], + handler, + ] + } } } diff --git a/jirs-client/webpack.config.js b/jirs-client/webpack.config.js index 93496801..b02dcc2d 100644 --- a/jirs-client/webpack.config.js +++ b/jirs-client/webpack.config.js @@ -3,11 +3,15 @@ const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); const HtmlWebpackPlugin = require('html-webpack-plugin'); const dotenv = require('dotenv'); const webpack = require('webpack'); +const { execSync, exec } = require('child_process'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); process.env.RUST_LOG = 'info'; +execSync('cd .. && cargo build --bin jirs-css'); +exec('cd .. && ./target/debug/jirs-css -O ./jirs-client/tmp/styles.css'); + dotenv.config(); module.exports = { diff --git a/jirs-css/src/main.rs b/jirs-css/src/main.rs index 19aac2fd..d6402698 100644 --- a/jirs-css/src/main.rs +++ b/jirs-css/src/main.rs @@ -1,85 +1,202 @@ +use std::collections::{HashMap, HashSet}; use std::fs::*; -use std::io::Read; use std::path::Path; +use std::sync::mpsc::channel; +use std::sync::{Arc, RwLock, RwLockWriteGuard}; +use std::time::Duration; use std::time::SystemTime; +use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; + const INPUT: &str = "./jirs-client/js/styles.css"; -#[derive(Debug, Default)] -struct Configuration { - input: String, - output: Option, - watch: bool, - prelude_selector: bool, - files: Vec, +type Css = Arc>; + +#[derive(Debug)] +enum Partial { + String(String), + File(Css), } -impl Configuration { - pub fn scan_fs(&mut self) -> Result<(), String> { - let input_dir = Path::new(self.input.as_str()) - .parent() - .ok_or_else(|| format!("Not a valid path {:?}", self.input))?; +#[derive(Debug)] +enum FileState { + Clean, + Dirty, + Dead, +} - let path = input_dir.to_str().unwrap(); - let paths = - glob::glob(format!("{}/**/*.css", path).as_str()).map_err(|e| format!("{}", e))?; - for path in paths.filter_map(Result::ok) { - self.files.push(path.display().to_string()); +#[derive(Debug)] +struct CssFile { + pub path: String, + pub lines: Vec, + pub last_changed: SystemTime, + pub state: FileState, +} + +impl CssFile { + pub fn new(path: String) -> Self { + Self { + path, + lines: vec![], + last_changed: SystemTime::UNIX_EPOCH, + state: FileState::Clean, + } + } +} + +impl std::fmt::Display for CssFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(format!("\n/* -- {} --- */\n\n", self.path).as_str())?; + for line in self.lines.iter() { + match line { + Partial::String(line) => { + f.write_str(line.as_str())?; + f.write_str("\n")?; + } + Partial::File(file) => { + if let Ok(css) = file.read() { + f.write_str(format!("{}", css).as_str())?; + f.write_str("\n")?; + } + } + } } Ok(()) } } -fn merge_files(input: &Path, out: &mut Vec) -> Result<(), String> { - let input_dir = input - .clone() - .parent() - .ok_or_else(|| format!("Not a valid path {:?}", input))?; - let contents: String = - read_to_string(input).map_err(|_| format!("File cannot be read {:?}", input))?; +#[derive(Debug, Default)] +struct Application { + input: String, + output: Option, + watch: bool, + prelude_selector: bool, + files_map: HashMap>, + fm: HashMap, + root_file: Option, +} - for line in contents.lines() { - if line.trim().is_empty() { - continue; +impl Application { + fn read_timestamp(input: &Path) -> Result { + std::fs::File::open(input) + .and_then(|file| file.metadata()) + .and_then(|meta| meta.modified()) + .map_err(|e| format!("{}", e)) + } + + fn check_timestamps( + &mut self, + input: &Path, + output_timestamp: SystemTime, + ) -> Result { + let input_dir = input + .clone() + .parent() + .ok_or_else(|| format!("Not a valid path {:?}", input))?; + + let path = input_dir.to_str().unwrap(); + let paths = + glob::glob(format!("{}/**/*.css", path).as_str()).map_err(|e| format!("{}", e))?; + for path in paths.filter_map(Result::ok) { + if Self::read_timestamp(path.as_path())? > output_timestamp { + return Ok(false); + } } - if !line.starts_with("@import ") { - out.push(line.to_string()); - continue; + Ok(true) + } + + fn parse(&mut self) -> Result<(), String> { + let root_path = self.input.to_string(); + let root = std::path::Path::new(&root_path); + let root_file = self.parse_file(root)?; + self.root_file = Some(root_file); + Ok(()) + } + + fn parse_file(&mut self, input: &Path) -> Result { + let file_path = input.display().to_string(); + let input_dir = input + .clone() + .parent() + .ok_or_else(|| format!("Not a valid path {:?}", input))?; + let file = self + .fm + .entry(file_path.clone()) + .or_insert_with(|| Arc::new(RwLock::new(CssFile::new(file_path.clone())))) + .clone(); + + if let Ok(mut css) = file.write() { + css.last_changed = Self::read_timestamp(input)?; } - let imported = { - line.replace("@import ", "") - .trim() - .replace("\"", "") - .replace(";", "") - .to_string() + + for line in read_to_string(file_path.as_str()) + .map_err(|e| format!("{}", e))? + .lines() + { + let l = line.trim(); + match l { + "" => continue, + _ if l.starts_with("@import ") => { + let imported = line + .replace("@import ", "") + .trim() + .replace("\"", "") + .replace(";", "") + .to_string(); + let child = input_dir + .clone() + .join(imported.as_str()) + .canonicalize() + .map_err(|e| format!("{}", e))?; + let child_file = self.parse_file(&child)?; + + if let Ok(mut css) = file.write() { + css.lines.push(Partial::File(child_file)); + } + } + _ => { + if let Ok(mut css) = file.write() { + css.lines.push(Partial::String(l.to_string())); + } + } + } + } + Ok(file) + } + + pub fn mark_dirty(&mut self, path: &Path) { + if let Ok(mut css) = self.css_at_path(path) { + css.state = FileState::Dirty; + } + } + + pub fn mark_dead(&mut self, path: &Path) { + if let Ok(mut css) = self.css_at_path(path) { + css.state = FileState::Dead; + } + } + + fn css_at_path(&mut self, path: &Path) -> Result, bool> { + self.fm + .get(path.display().to_string().as_str()) + .ok_or(false) + .and_then(|css| css.write().or(Err(false))) + } + + fn print(&self) { + let css = match self.root_file.as_ref().unwrap().read() { + Ok(css) => css, + _ => return, }; - let child = input_dir.clone().join(imported.as_str()); - merge_files(child.as_path(), out)?; - } - Ok(()) -} - -fn read_timestamp(input: &Path) -> Result { - std::fs::File::open(input) - .and_then(|file| file.metadata()) - .and_then(|meta| meta.modified()) - .map_err(|e| format!("{}", e)) -} - -fn check_timestamps(input: &Path, output_timestamp: SystemTime) -> Result { - let input_dir = input - .clone() - .parent() - .ok_or_else(|| format!("Not a valid path {:?}", input))?; - - let path = input_dir.to_str().unwrap(); - let paths = glob::glob(format!("{}/**/*.css", path).as_str()).map_err(|e| format!("{}", e))?; - for path in paths.filter_map(Result::ok) { - if read_timestamp(path.as_path())? > output_timestamp { - return Ok(false); + match self.output.as_ref() { + Some(f) => { + std::fs::create_dir_all(Path::new(f).parent().unwrap()).unwrap(); + std::fs::write(f, format!("{}", css)).unwrap(); + println!("CSS merge done"); + } + _ => println!("{}", css), } } - Ok(true) } fn main() -> Result<(), String> { @@ -99,15 +216,17 @@ fn main() -> Result<(), String> { ) .get_matches(); - let mut config = Configuration { + let mut app = Application { input: matches.value_of("input").unwrap().to_string(), output: matches.value_of("output").map(|s| s.to_string()), watch: matches.is_present("watch"), prelude_selector: matches.is_present("prelude"), - files: vec![], + files_map: Default::default(), + fm: Default::default(), + root_file: None, }; - config.scan_fs()?; - println!("{:?}", config); + let root_path = app.input.to_string(); + let root = std::path::Path::new(&root_path); let output_timestamp = matches .value_of("output") @@ -117,28 +236,37 @@ fn main() -> Result<(), String> { .and_then(|meta| meta.modified()) .unwrap_or_else(|_| SystemTime::UNIX_EPOCH.clone()); - let mut file = - std::fs::File::open(matches.value_of("input").unwrap()).map_err(|e| format!("{}", e))?; - let input = matches.value_of("input").unwrap(); - - if check_timestamps(Path::new(input), output_timestamp)? { + if app.check_timestamps(root, output_timestamp)? { return Ok(()); } - let mut contents: String = String::new(); - file.read_to_string(&mut contents) - .map_err(|e| format!("{}", e))?; + app.parse()?; + app.print(); - let mut out = vec![]; - merge_files(std::path::Path::new(input), &mut out)?; + let (tx, rx) = channel(); + let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); - match matches.value_of("output") { - Some(output) => { - std::fs::create_dir_all(Path::new(output).parent().unwrap()).unwrap(); - std::fs::write(output, out.join("\n")).unwrap() - } - None => println!("{}", out.join("\n")), + for file in app.fm.keys() { + watcher + .watch(file.to_string(), RecursiveMode::NonRecursive) + .unwrap(); } - Ok(()) + loop { + match rx.recv() { + Ok(DebouncedEvent::NoticeWrite(path)) => { + app.mark_dirty(path.as_path()); + if let Err(s) = app.parse_file(&path) { + eprintln!("{}", s); + } + app.print(); + } + Ok(DebouncedEvent::NoticeRemove(path)) => { + app.mark_dead(path.as_path()); + watcher.unwatch(path).unwrap(); + } + Ok(event) => println!("{:?}", event), + Err(e) => println!("watch error: {:?}", e), + } + } }