├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── adapters ├── Cargo.toml └── src │ ├── github_ticket_adapter │ ├── adapter.rs │ └── mod.rs │ ├── lib.rs │ └── local_ticket_adapter │ ├── adapter.rs │ ├── interpreter.rs │ ├── interpreter_errors.rs │ ├── interpreter_instructions.rs │ ├── interpreter_parameters.rs │ ├── interpreter_tests.rs │ └── mod.rs ├── assets ├── adapters │ ├── database.png │ └── github.png ├── icon_app.png └── screenshot_app.png ├── core ├── Cargo.toml └── src │ ├── adapter_base │ ├── adapter_error.rs │ ├── mod.rs │ └── ticket_adapter.rs │ ├── data_model │ ├── bucket.rs │ ├── bucket_panel_location.rs │ ├── config │ │ ├── app_config.rs │ │ ├── config_base.rs │ │ ├── config_option.rs │ │ └── mod.rs │ ├── data_model_tests.rs │ ├── filter.rs │ ├── local_database.rs │ ├── mod.rs │ ├── state.rs │ ├── tag.rs │ └── ticket.rs │ ├── lib.rs │ └── ticket_provider │ └── mod.rs ├── main.rs └── ui ├── Cargo.toml └── src ├── helper.rs ├── lib.rs ├── overlays ├── helper.rs ├── mod.rs ├── overlay_about.rs ├── overlay_adapter.rs ├── overlay_bucket.rs ├── overlay_filter.rs ├── overlay_preferences.rs ├── overlay_state.rs ├── overlay_tag.rs ├── overlay_ticket.rs └── overlay_wizard.rs ├── ui_cache.rs ├── ui_controller.rs ├── ui_theme.rs └── user_interface ├── menu_bar.rs ├── mod.rs ├── side_panel.rs └── ticket.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /*.db3 3 | .vscode/launch.json 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | rust-version = "1.70" 3 | name = "tickets-rs" 4 | version = "0.1.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [[bin]] 9 | name = "tickets-rs" 10 | edition = "2021" 11 | path = "main.rs" 12 | 13 | [workspace] 14 | members = [ 15 | "adapters", 16 | "core", 17 | "ui" 18 | ] 19 | 20 | [dependencies] 21 | tickets-rs-core = { version = "0.1", path = "core" } 22 | tickets-rs-adapters = { version = "0.1", path = "adapters" } 23 | tickets-rs-ui = { version = "0.1", path = "ui" } 24 | 25 | tokio = {version = "1.32.0", features = ["full"] } 26 | 27 | [profile.release-opt] 28 | inherits = "release" 29 | codegen-units = 1 30 | debug = false 31 | lto = true 32 | incremental = false 33 | opt-level = 3 34 | overflow-checks = false 35 | strip = "debuginfo" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tickets.rs - A Ticket Management Tool 2 | 3 | Tickets.rs is a Desktop application made with egui, that is used for managing and modifying Tickets (or Issues, Tasks, TODO's). It's goal is to have the choice between multiple different Adapters, that can interface with different 4 | Systems out there, or locally to keep everything in one place: **this App**. 5 | 6 | ![App Screenshot](assets/screenshot_app.png) 7 | 8 | Currently supported Language is **English** only. 9 | 10 | ## Features 11 | 12 | * Written in _100% safe Rust_. 13 | * Manage, edit and move Tickets from different Sources in one place 14 | * Add Tags to Tickets, to make them easily sortable 15 | * Create Categories or "Buckets" to sort Tickets in 16 | * Create Filters to view specific Combinations of Tickets, even across multiple Adapters 17 | * Write and view Descriptions with Commonmark (similar to Markdown) 18 | * Change the appearance of the Tool on the Fly (including Font Size and custom Colors) 19 | * Remove and Add Ticket Sources or "Adapters" to and from the App without needing to restart 20 | * Includes a little Wizard to get started quickly 21 | 22 | ## Currently Supported Adapters 23 | * _local:_ Tickets are stored on the Computers Hard Drive in the form of a SQLite Database. In Theory you can put this File on a Network Folder, but i haven't tested, how it reacts to being already locked. I use it via sshfs successfully. 24 | * _github:_ Ticket are being read from Github and cached (updated every 5 Minutes on request). There are no Filters yet, and it is purely read only. You need to supply a personal Access Token. So far i only managed to get all my Public Repositories to show (not sure, if that's any different with different Plans). However, you can also display the Repo List of other accounts via this Adapter. It is very barebones, doesn't include any Comments or details of why an issue is open/closed and no Pull Requests. 25 | 26 | ## Getting Started (from Source) 27 | 28 | The Program is written in Rust, therefore you need to have all the necessary Tools for it installed. 29 | It is rather easy though: 30 | 31 | * if you haven't installed Rust or Cargo, do what [this guide](https://doc.rust-lang.org/cargo/getting-started/installation.html) says. 32 | * Download this Repository as a .zip File and extract it or clone it with: `git clone`. (Requires git) 33 | * Open up the Terminal and navigate into the Folder you just downloaded until you have found the directory with the _Cargo.toml_ in there and type `cargo build --release` 34 | * After the build is done, there should be a new Folder called _target/release_, which will contain an executable called _tickets-rs_. Copy this file into a folder of your choice and run it. 35 | * If you navigate out of the _target_ folder, you should find an _assets_ folder. Copy it into the same folder as the executable. It contains all the images, the app needs. 36 | * _(Optional)_ make a shortcut to the executable. 37 | 38 | ## Getting started (with Binaries) 39 | 40 | There is a new branch available called _releases_. In there you should be able to find the zip files of the binaries, so you can just download the version you need, unpack it and run it. 41 | 42 | * [Go to Windows 10+ Releases Folder](https://github.com/TheBiochemic/tickets-rs/tree/releases/win_versions) 43 | * [Go to Ubuntu Releases Folder](https://github.com/TheBiochemic/tickets-rs/tree/releases/ubuntu_versions) 44 | 45 | ## Contributors 46 | 47 | The Tool is written by me, _Robert Lang (Biochemic)_ as a Project to learn Rust and advance my 48 | prototype of an already personally built and used Software in Python. 49 | 50 | ## License 51 | 52 | You find the License in the LICENSE File inside of this Repository. But as tl;dr it's GPLv3. 53 | -------------------------------------------------------------------------------- /adapters/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | rust-version = "1.70" 3 | name = "tickets-rs-adapters" 4 | version = "0.1.0" 5 | authors = ["Robert Lang"] 6 | edition = "2021" 7 | description = "This Crate contains the implementations of the default adapters for tickets-rs" 8 | 9 | [dependencies] 10 | tickets-rs-core = { version = "0.1", path = "../core" } 11 | 12 | rusqlite = { version = "0.28.0", features = ["bundled"] } 13 | octocrab = { version = "0.31.0" } 14 | tokio = { version = "1.32.0" } 15 | reqwest = { version = "0.11.22", features = ["blocking", "json"] } -------------------------------------------------------------------------------- /adapters/src/github_ticket_adapter/adapter.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::{Arc, Mutex}, time::{Instant, Duration, SystemTime}, collections::BTreeMap, thread}; 2 | 3 | use octocrab::{Octocrab, models}; 4 | use tickets_rs_core::{TicketAdapter, TicketProvider, AppConfig, Config, AdapterError, AdapterErrorType, Filter, Ticket}; 5 | use tokio::runtime::Handle; 6 | 7 | use crate::GithubTicketAdapter; 8 | 9 | 10 | impl TicketAdapter for GithubTicketAdapter { 11 | 12 | fn get_type_name() -> String where Self: Sized { 13 | "github".to_string() 14 | } 15 | 16 | fn get_fancy_type_name() -> String where Self: Sized { 17 | "Github Issues".to_string() 18 | } 19 | 20 | fn get_name(&self) -> String { 21 | self.name.clone() 22 | } 23 | 24 | fn create_config() -> tickets_rs_core::Config where Self: Sized { 25 | TicketProvider::get_default_config::() 26 | .with("personal_auth_token", "", "string") 27 | .with("repo_owner", "", "string") 28 | } 29 | 30 | fn from_config(app_config: Arc>, config: &Config, finished: Arc>) -> Result, AdapterError> where Self: Sized { 31 | 32 | let mut auth_token = "".to_string(); 33 | 34 | let octocrab = match config.get("personal_auth_token") { 35 | Some(config_option) => { 36 | 37 | match config_option.get::() { 38 | Some(config_string) => { 39 | if !config_string.is_empty() { 40 | auth_token = config_string.clone(); 41 | match Octocrab::builder().personal_token(config_string).build() { 42 | Ok(instance) => { 43 | Arc::new(instance) 44 | }, 45 | Err(err) => { 46 | println!("{}", err); 47 | return Err(AdapterError { error_type: AdapterErrorType::Instantiation }) 48 | }, 49 | } 50 | } else { 51 | octocrab::instance() 52 | } 53 | }, 54 | None => { 55 | octocrab::instance() 56 | }, 57 | } 58 | 59 | }, 60 | None => { 61 | octocrab::instance() 62 | }, 63 | }; 64 | 65 | let owner: String = match config.get("repo_owner") { 66 | Some(option) => match option.get() { 67 | Some(result) => result, 68 | None => return Err(AdapterError::new(AdapterErrorType::Instantiation)), 69 | }, 70 | None => return Err(AdapterError::new(AdapterErrorType::Instantiation)), 71 | }; 72 | 73 | let name: String = match config.get("name") { 74 | Some(option) => match option.get() { 75 | Some(result) => result, 76 | None => return Err(AdapterError::new(AdapterErrorType::Instantiation)), 77 | }, 78 | None => return Err(AdapterError::new(AdapterErrorType::Instantiation)), 79 | }; 80 | 81 | let display_name: String = match config.get("display") { 82 | Some(option) => match option.get() { 83 | Some(result) => result, 84 | None => return Err(AdapterError::new(AdapterErrorType::Instantiation)), 85 | }, 86 | None => return Err(AdapterError::new(AdapterErrorType::Instantiation)), 87 | }; 88 | 89 | let instant = match Instant::now().checked_sub(Duration::from_secs(600)) { 90 | Some(instant) => instant, 91 | None => Instant::now(), 92 | }; 93 | 94 | let mut adapter = GithubTicketAdapter{ 95 | name, 96 | display_name, 97 | config: app_config, 98 | cached_tickets: Default::default(), 99 | cached_buckets: Default::default(), 100 | cached_tags: Default::default(), 101 | cached_states: Default::default(), 102 | octocrab, 103 | auth_token, 104 | last_refresh: instant, 105 | owner, 106 | update_trigger: Arc::new(Mutex::new(false)) 107 | }; 108 | 109 | adapter.full_refresh_data(finished); 110 | 111 | Ok(Box::new(adapter)) 112 | 113 | } 114 | 115 | fn get_icon(&self) -> &std::path::Path { 116 | std::path::Path::new("assets/adapters/github.png") 117 | } 118 | 119 | fn get_fancy_name(&self) -> String { 120 | self.display_name.clone() 121 | } 122 | 123 | fn is_read_only(&self) -> bool { 124 | true //TODO! 125 | } 126 | 127 | fn bucket_list_all(&self) -> Vec { 128 | if let Ok(lock) = self.cached_buckets.lock() { 129 | return lock.values().cloned().collect(); 130 | }; 131 | 132 | vec![] 133 | } 134 | 135 | fn bucket_list_unique(&self, id: u64) -> Option { 136 | if let Ok(lock) = self.cached_buckets.lock() { 137 | return lock.get(&id).cloned(); 138 | } 139 | 140 | None 141 | } 142 | 143 | fn bucket_drop(&self, bucket: &tickets_rs_core::Bucket) -> Result<(), tickets_rs_core::AdapterError> { 144 | Err(tickets_rs_core::AdapterError::new(tickets_rs_core::AdapterErrorType::BucketDelete)) //TODO! 145 | } 146 | 147 | fn bucket_write(&self, bucket: &mut tickets_rs_core::Bucket) -> Result<(), tickets_rs_core::AdapterError> { 148 | Err(tickets_rs_core::AdapterError::new(tickets_rs_core::AdapterErrorType::BucketWrite)) //TODO! 149 | } 150 | 151 | fn ticket_list_all(&self) -> Vec { 152 | 153 | let buckets = if let Ok(lock) = self.cached_buckets.lock() { 154 | 155 | lock.values().cloned().collect() 156 | 157 | } else { 158 | vec![] 159 | }; 160 | 161 | let mut tickets: Vec = vec![]; 162 | 163 | for bucket in buckets { 164 | match self.ticket_list(&Self::filter_expr_from_bucket(&bucket)) { 165 | Ok(mut new_tickets) => tickets.append(&mut new_tickets), 166 | Err(_) => (), 167 | } 168 | } 169 | 170 | tickets 171 | } 172 | 173 | fn ticket_list_unique(&self, id: i64) -> Option { 174 | 175 | let mut local_ticket_opt = None; 176 | 177 | // Check if the ticket even exists locally 178 | if let Ok(tickets_lock) = self.cached_tickets.lock() { 179 | local_ticket_opt = tickets_lock.get(&(id as u64)).cloned() 180 | } 181 | 182 | // If there was some Ticket found, get the bucket name as repo name and query the ticket 183 | if let Some(local_ticket) = local_ticket_opt { 184 | 185 | if let Some(local_bucket) = self.bucket_list_unique(local_ticket.bucket_id) { 186 | 187 | let thread_octocrab = self.octocrab.clone(); 188 | let thread_owner = self.owner.clone(); 189 | let thread_id = { 190 | let add_id_str = local_ticket.additional_id.clone(); 191 | //let add_id: &str = &add_id_str; 192 | let add_id_opt = add_id_str.split_once("::"); 193 | let num = add_id_opt.unwrap().0.to_string(); 194 | num 195 | }; 196 | let thread_repo = local_bucket.name.clone(); 197 | let thread_repo_id = local_bucket.identifier.clone(); 198 | let thread_ticket_proto = Ticket::default().with_adapter(self); 199 | let handle = Handle::current(); 200 | let thread_result = thread::spawn(move || { 201 | 202 | if let Ok(issue) = handle.block_on(thread_octocrab.issues(thread_owner, thread_repo.clone()).get(u64::from_str_radix(&thread_id, 10).unwrap())) { 203 | Self::map_issues_to_tickets(vec![issue], thread_ticket_proto, thread_repo_id.id, &thread_repo).pop_first().map(|elem| elem.1) 204 | } else { 205 | println!("wasnt able to get issue by id {}", thread_id); 206 | None 207 | } 208 | 209 | }).join(); 210 | 211 | if let Ok(thread_inner_data) = thread_result { 212 | if let Some(ticket) = &thread_inner_data { 213 | 214 | if let Ok(mut tickets_lock) = self.cached_tickets.lock() { 215 | tickets_lock.insert(ticket.id as u64, ticket.clone()); 216 | }; 217 | 218 | }; 219 | 220 | thread_inner_data 221 | } else { 222 | println!("thread didnt exit correctly"); 223 | None 224 | } 225 | } else { 226 | println!("didnt find bucket"); 227 | None 228 | } 229 | } else { 230 | println!("didnt find locally stored ticket"); 231 | None 232 | } 233 | 234 | } 235 | 236 | fn ticket_list(&self, expression: &str) -> Result, tickets_rs_core::AdapterError> { 237 | 238 | let split_expression: Vec<&str> = expression.split(" ||| ").collect(); 239 | let repo = split_expression.get(0).unwrap().to_string(); 240 | let id = u64::from_str_radix(split_expression.get(1).unwrap(), 10).unwrap(); 241 | 242 | let mut loaded = true; 243 | 244 | if let Ok(mut lock) = self.cached_buckets.lock() { 245 | if let Some(result) = lock.get_mut(&id) { 246 | match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 247 | Ok(duration) => { 248 | let last_change_ts = result.last_change as u64; 249 | let diff = duration.as_secs() - last_change_ts; 250 | 251 | if diff > 5 * 60 { 252 | loaded = false; 253 | result.last_change = duration.as_secs() as i64; 254 | } 255 | }, 256 | Err(_err) => (), 257 | }; 258 | } 259 | } 260 | 261 | if !loaded { 262 | 263 | let handle = Handle::current(); 264 | 265 | let thread_octocrab = self.octocrab.clone(); 266 | let thread_owner = self.owner.clone(); 267 | let thread_ticket_proto = Ticket::default().with_adapter(self); 268 | let thread_cached_tickets = self.cached_tickets.clone(); 269 | let final_result = thread::spawn(move || { 270 | 271 | let result_page = handle.block_on(thread_octocrab.issues(thread_owner.clone(), repo.clone()).list().state(octocrab::params::State::All).per_page(100).send()); 272 | match result_page { 273 | Ok(found_page) => { 274 | let issues_result = handle.block_on(thread_octocrab.all_pages::(found_page)); 275 | 276 | if let Ok(issues) = issues_result { 277 | let tickets: BTreeMap = Self::map_issues_to_tickets(issues, thread_ticket_proto, id, &repo); 278 | 279 | if let Ok(mut lock) = thread_cached_tickets.lock() { 280 | lock.retain(|key, ticket| id.ne(&ticket.bucket_id) ); 281 | lock.append(&mut tickets.clone()); 282 | let local_tickets: Vec = tickets.into_iter().map(|ticket| ticket.1).collect(); 283 | return Some(local_tickets) 284 | } else { 285 | return None 286 | } 287 | 288 | } 289 | }, 290 | Err(_) => (), 291 | } 292 | 293 | return None 294 | 295 | 296 | }).join(); 297 | 298 | match final_result { 299 | Ok(tickets_vec) => { 300 | match tickets_vec { 301 | Some(inner_vec) => Ok(inner_vec), 302 | None => Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::Access}), 303 | } 304 | }, 305 | Err(_) => Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::Access}), 306 | } 307 | 308 | 309 | } else { 310 | if let Ok(lock) = self.cached_tickets.lock() { 311 | 312 | let local_tickets: Vec = lock 313 | .iter() 314 | .filter(|ticket| {ticket.1.bucket_id.eq(&id)}) 315 | .map(|ticket| ticket.1) 316 | .cloned() 317 | .collect(); 318 | 319 | return Ok(local_tickets) 320 | } 321 | Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::Access}) 322 | } 323 | 324 | 325 | } 326 | 327 | fn ticket_write(&self, ticket: &tickets_rs_core::Ticket) -> Result<(), tickets_rs_core::AdapterError> { 328 | Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::TicketWrite}) 329 | } 330 | 331 | fn ticket_drop(&self, ticket: &tickets_rs_core::Ticket) -> Result<(), tickets_rs_core::AdapterError> { 332 | Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::TicketDelete}) 333 | } 334 | 335 | fn state_list_all(&self) -> Vec { 336 | if let Ok(lock) = self.cached_states.lock() { 337 | return lock.values().cloned().collect(); 338 | }; 339 | 340 | vec![] 341 | } 342 | 343 | fn state_write(&self, state: &tickets_rs_core::State) -> Result<(), tickets_rs_core::AdapterError> { 344 | Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::StateWrite}) 345 | } 346 | 347 | fn tag_list_all(&self) -> Vec { 348 | if let Ok(lock) = self.cached_tags.lock() { 349 | return lock.values().cloned().collect(); 350 | }; 351 | 352 | vec![] 353 | } 354 | 355 | fn tag_write(&self, state: &tickets_rs_core::Tag) -> Result<(), tickets_rs_core::AdapterError> { 356 | Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::TagWrite}) 357 | } 358 | 359 | fn tag_drop(&self, state: &tickets_rs_core::Tag) -> Result<(), tickets_rs_core::AdapterError> { 360 | Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::TagDelete}) 361 | } 362 | 363 | fn filter_list_all(&self) -> Vec { 364 | 365 | let mut filters: Vec = Vec::new(); 366 | 367 | filters.append(&mut self.list_builtin_filters()); 368 | 369 | filters 370 | } 371 | 372 | fn filter_list(&self, filter_name: String) -> Option { 373 | None 374 | } 375 | 376 | fn filter_write(&self, filter: &tickets_rs_core::Filter) -> Result<(), tickets_rs_core::AdapterError> { 377 | Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::FilterWrite}) 378 | } 379 | 380 | fn filter_drop(&self, filter: &tickets_rs_core::Filter) -> Result<(), tickets_rs_core::AdapterError> { 381 | Err(tickets_rs_core::AdapterError{error_type: AdapterErrorType::FilterDelete}) 382 | } 383 | 384 | fn filter_expression_validate(&self, expression: &String) -> Result<(), Vec<(String, String)>> { 385 | let split_expression: Vec<&str> = expression.split(" ||| ").collect(); 386 | if split_expression.len() == 2 { 387 | 388 | match u64::from_str_radix(split_expression.get(1).unwrap(), 10) { 389 | Ok(_) => Ok(()), 390 | Err(_) => Err(vec![("operation".to_string(), "Expression needs to be in the form of \"repo_name ||| repo_id\". repo_id needs to be a number".to_string())]), 391 | } 392 | 393 | } else { 394 | Err(vec![("operation".to_string(), "Expression needs to be in the form of \"repo_name ||| repo_id\"".to_string())]) 395 | } 396 | } 397 | 398 | } -------------------------------------------------------------------------------- /adapters/src/github_ticket_adapter/mod.rs: -------------------------------------------------------------------------------- 1 | mod adapter; 2 | use std::{collections::BTreeMap, sync::{Arc, Mutex}, time::{Instant, Duration, SystemTime}, thread}; 3 | 4 | pub use adapter::*; 5 | use octocrab::{Octocrab, models, Page}; 6 | use reqwest::header::{HeaderMap, HeaderValue, self}; 7 | use tickets_rs_core::{AppConfig, Ticket, Bucket, BucketIdentifier, Filter, FilterType, TicketAdapter, Tag, State}; 8 | use tokio::runtime::Handle; 9 | 10 | pub struct GithubTicketAdapter { 11 | name: String, 12 | display_name: String, 13 | config: Arc>, 14 | cached_tickets: Arc>>, 15 | cached_buckets: Arc>>, // The bool is for seeing, if the corresponding issues need to be loaded 16 | cached_tags: Arc>>, 17 | cached_states: Arc>>, 18 | octocrab: Arc, 19 | auth_token: String, 20 | last_refresh: Instant, 21 | owner: String, 22 | update_trigger: Arc> 23 | } 24 | 25 | impl GithubTicketAdapter { 26 | pub(crate) fn full_refresh_data(&mut self, update_trigger: Arc>) { 27 | 28 | // Try to limit updates, so that the API is not getting spammed all the time 29 | if self.last_refresh.elapsed() < Duration::from_secs(5 * 60) { 30 | return; 31 | } 32 | 33 | self.last_refresh = Instant::now(); 34 | self.update_trigger = update_trigger.clone(); 35 | 36 | let thread_buckets = self.cached_buckets.clone(); 37 | let thread_tags = self.cached_tags.clone(); 38 | let thread_states = self.cached_states.clone(); 39 | let thread_octocrab = self.octocrab.clone(); 40 | let thread_owner = self.owner.clone(); 41 | let thread_bucket_proto = Bucket::default().with_adapter(self); 42 | let thread_tag_proto = Tag::default().with_adapter(self); 43 | let thread_state_proto = State::default().with_adapter(self); 44 | let thread_auth_token = self.auth_token.clone(); 45 | let handle = Handle::current(); 46 | let _ = thread::spawn(move || { 47 | 48 | let users_request = thread_octocrab.users(thread_owner.clone()); 49 | 50 | // First get all the repos available in the account 51 | let repos_page = match handle.block_on(users_request.repos().per_page(100).send()) { 52 | Ok(page) => page, 53 | Err(err) => { 54 | println!("{}", err); 55 | return; 56 | } 57 | }; 58 | let repos_result = handle.block_on(thread_octocrab.all_pages::(repos_page)); 59 | 60 | let mut local_cached_buckets: BTreeMap = BTreeMap::default(); 61 | let mut local_cached_tags: BTreeMap = BTreeMap::default(); 62 | 63 | match repos_result { 64 | Ok(repos) => { 65 | 66 | for repo in repos { 67 | 68 | let mut local_bucket = thread_bucket_proto.clone(); 69 | local_bucket = local_bucket.with_details(repo.id.0, repo.name); 70 | 71 | local_bucket.last_change = 0u64 as i64; // Is still unloaded, that's why the last_change is 0 72 | 73 | local_cached_buckets.insert(repo.id.0, local_bucket.clone()); 74 | 75 | }; 76 | 77 | 78 | }, 79 | Err(err) => { 80 | println!("{}", err); 81 | return; 82 | } 83 | } 84 | 85 | match thread_buckets.lock() { 86 | Ok(mut lock) => { 87 | lock.clear(); 88 | lock.append(&mut local_cached_buckets.clone()); 89 | }, 90 | Err(_) => (), 91 | } 92 | 93 | // Now get all labels and map them to tags 94 | for buckets in local_cached_buckets { 95 | 96 | let mut headers = HeaderMap::new(); 97 | headers.insert("X-GitHub-Api-Version", HeaderValue::from_str("2022-11-28").unwrap()); 98 | headers.insert(header::ACCEPT, HeaderValue::from_str("application/vnd.github+json").unwrap()); 99 | headers.insert(header::USER_AGENT, HeaderValue::from_static("curl/7.54.1")); 100 | 101 | let client = reqwest::blocking::Client::new(); 102 | let request = client 103 | .get(format!("https://api.github.com/repos/{}/{}/labels", thread_owner, buckets.1.name)) 104 | .bearer_auth(thread_auth_token.clone()) 105 | .headers(headers) 106 | .build().unwrap(); 107 | 108 | let response_result = client.execute(request); 109 | 110 | match response_result { 111 | Ok(response) => { 112 | let parsed = response.json::>(); 113 | match parsed { 114 | Ok(parsed_vec) => { 115 | for label in parsed_vec { 116 | //println!("{}, {}", label.name, label.color); 117 | let next_tag = thread_tag_proto.clone() 118 | .with_name(label.name.clone()) 119 | .with_hex_color(label.color.as_str()); 120 | local_cached_tags.insert(label.name, next_tag); 121 | } 122 | }, 123 | Err(err) => println!("parse vec: {}", err), 124 | } 125 | }, 126 | Err(err) => println!("parse body: {}", err), 127 | } 128 | 129 | }; 130 | 131 | match thread_states.lock() { 132 | Ok(mut lock) => { 133 | lock.clear(); 134 | lock.insert("open".into(), thread_state_proto.clone().with_name("open".into()).with_description("This issue is still open.".into())); 135 | lock.insert("closed".into(), thread_state_proto.clone().with_name("closed".into()).with_description("This issue has been closed.".into())); 136 | println!("appended {} states", lock.len()); 137 | }, 138 | Err(_) => (), 139 | } 140 | 141 | match thread_tags.lock() { 142 | Ok(mut lock) => { 143 | lock.clear(); 144 | lock.append(&mut local_cached_tags); 145 | println!("appended {} tags", lock.len()); 146 | }, 147 | Err(_) => (), 148 | } 149 | 150 | 151 | 152 | if let Ok(mut lock) = update_trigger.lock() { 153 | *lock = true; 154 | }; 155 | 156 | }); 157 | 158 | } 159 | 160 | fn map_issues_to_tickets(issues: Vec, ticket_proto: Ticket, bucket_id: u64, bucket_name: &str) -> BTreeMap { 161 | issues.into_iter().map(|issue| { 162 | 163 | // Create Ticket with adapter 164 | let mut ticket = ticket_proto.clone(); 165 | 166 | // Add Assignees 167 | let assignees = issue.assignees.into_iter().map(|author| author.login).collect::>().join(", "); 168 | ticket = ticket.with_assignee(assignees); 169 | 170 | // Add State 171 | ticket.state_name = match issue.state { 172 | models::IssueState::Open => "open".to_string(), 173 | models::IssueState::Closed => "closed".to_string(), 174 | _ => "open".to_string(), 175 | }; 176 | 177 | // Add Id and Title 178 | ticket = ticket.with_details(issue.id.0 as i64, issue.title, "".to_string()); 179 | 180 | 181 | // Add Description 182 | if let Some(body) = issue.body { 183 | ticket.description = body; 184 | } 185 | 186 | // Add Tags 187 | ticket.tags = issue.labels.iter().map(|label| { 188 | label.name.clone() 189 | }).collect::>(); 190 | 191 | // With Bucket Id 192 | ticket.bucket_id = bucket_id; 193 | 194 | //Create additional id 195 | ticket.additional_id = format!("{}::{}", issue.number, bucket_name); 196 | 197 | (issue.id.0, ticket) 198 | 199 | }).collect() 200 | } 201 | 202 | fn list_builtin_filters(&self) -> Vec { 203 | 204 | let buckets = self.bucket_list_all(); 205 | 206 | buckets.iter().map(|bucket| { 207 | Filter::default() 208 | .with_details( 209 | bucket.name.clone(), 210 | Filter::filter_expression(self.get_name(), &Self::filter_expr_from_bucket(bucket))) 211 | .with_type(FilterType::Bucket(bucket.identifier.id)) 212 | .with_adapter(self) 213 | }).collect::>() 214 | } 215 | 216 | pub(crate) fn filter_expr_from_bucket(bucket: &Bucket) -> String { 217 | format!("{} ||| {}", bucket.name.clone(), bucket.identifier.id.to_string()) 218 | } 219 | } -------------------------------------------------------------------------------- /adapters/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod local_ticket_adapter; 2 | mod github_ticket_adapter; 3 | 4 | pub use local_ticket_adapter::LocalTicketAdapter; 5 | pub use github_ticket_adapter::GithubTicketAdapter; -------------------------------------------------------------------------------- /adapters/src/local_ticket_adapter/interpreter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::fmt::{ 3 | Display, Result as FmtResult, Formatter 4 | }; 5 | use std::sync::{Mutex, Arc}; 6 | 7 | use tickets_rs_core::AppConfig; 8 | 9 | pub use super::interpreter_errors::{ 10 | TokenizationError, 11 | NewTokenizationError, 12 | SqlParseError, 13 | NewSqlParseError 14 | }; 15 | use super::interpreter_instructions::{ 16 | Instruction, 17 | VerifiableInstruction 18 | }; 19 | 20 | pub struct SqlExpression { 21 | finished_expression: String, 22 | from_expression: Vec, 23 | where_expression: Vec 24 | } 25 | 26 | impl Default for SqlExpression { 27 | fn default() -> Self { 28 | SqlExpression { 29 | finished_expression: "".to_string(), 30 | from_expression: vec![], 31 | where_expression: vec![], 32 | } 33 | } 34 | } 35 | 36 | impl SqlExpression { 37 | 38 | pub fn add_to_from(&mut self, expression: String) { 39 | 40 | if !self.from_expression.contains(&expression) { 41 | self.from_expression.push(expression); 42 | } 43 | } 44 | 45 | pub fn add_to_where(&mut self, expression: String) { 46 | self.where_expression.push(expression); 47 | } 48 | 49 | pub fn _add_directly(&mut self, expression: String) { 50 | self.finished_expression += expression.as_str(); 51 | } 52 | 53 | pub fn flush(&mut self) { 54 | 55 | match self.finished_expression.is_empty() { 56 | true => self.finished_expression += "SELECT tickets.* FROM ", 57 | false => self.finished_expression += " UNION SELECT tickets.* FROM ", 58 | } 59 | 60 | let mut join_expression = "tickets".to_string(); 61 | 62 | for from in self.from_expression.as_slice() { 63 | join_expression = [ 64 | "(", 65 | join_expression.as_str(), 66 | ") JOIN ", 67 | from.as_str() 68 | ].join("") 69 | } 70 | 71 | self.finished_expression += join_expression.as_str(); 72 | 73 | if !self.where_expression.is_empty() { 74 | self.finished_expression += " WHERE "; 75 | self.finished_expression += self.where_expression.join(" AND ").as_str(); 76 | } 77 | 78 | self.where_expression.clear(); 79 | self.from_expression.clear(); 80 | 81 | } 82 | 83 | pub fn is_buffer_empty(&self) -> bool { 84 | self.finished_expression.is_empty() && self.from_expression.is_empty() && self.where_expression.is_empty() 85 | } 86 | 87 | pub fn is_accumulator_empty(&self) -> bool { 88 | self.from_expression.is_empty() && self.where_expression.is_empty() 89 | } 90 | 91 | pub fn get_final(&mut self) -> &String { 92 | self.finished_expression.push(';'); 93 | &self.finished_expression 94 | } 95 | } 96 | 97 | pub trait SqlParsable: Sized { 98 | fn to_sql(&self, interpreter: &AdapterInterpreter, sql_expression: SqlExpression) -> Result; 99 | } 100 | 101 | pub struct AdapterInterpreter { 102 | instructions: VecDeque, 103 | variables: HashMap, 104 | last_error: Option, 105 | pub can_have_title_contains: bool, 106 | pub can_have_descr_contains: bool, 107 | pub can_have_due_in_days: bool 108 | } 109 | 110 | impl AdapterInterpreter { 111 | 112 | pub fn has_variable(&self, variable_name: &String) -> bool{ 113 | self.variables.contains_key(variable_name) 114 | } 115 | 116 | pub fn get_variable(&self, variable_name: &String) -> Option<&String> { 117 | self.variables.get(variable_name) 118 | } 119 | 120 | pub fn set_variable(&mut self, variable_name: &str, value: &str) { 121 | self.variables.insert(String::from(variable_name), String::from(value)); 122 | } 123 | 124 | #[cfg(test)] 125 | pub fn get_last_error(&self) -> Option{ 126 | self.last_error.clone() 127 | } 128 | 129 | pub fn setup_environment(&mut self, config: Arc>) { 130 | match config.lock() { 131 | Ok(mut config) => { 132 | self.set_variable("me", config.get_or_default( 133 | "username", "new User", "" 134 | ).raw().as_str()); 135 | }, 136 | Err(err) => println!("Wasn't able to lock Config. Reason: {}", err), 137 | } 138 | } 139 | 140 | pub fn try_tokenize(&mut self, code: String) -> Result<(), TokenizationError> { 141 | self.instructions.clear(); 142 | let mut code_internal = code.trim_start().to_string(); 143 | while !code_internal.is_empty() { 144 | 145 | if let Some(token_error) = match Instruction::try_tokenize(self, code_internal.clone()) { 146 | Ok(verified_instr) => { 147 | code_internal = verified_instr.1; 148 | self.instructions.push_back(verified_instr.0); 149 | None 150 | }, 151 | Err(token_error) => Some(token_error), 152 | } { 153 | self.last_error = Some(token_error.clone()); 154 | self.instructions.clear(); 155 | return Err(token_error); 156 | } 157 | 158 | code_internal = code_internal.trim_start().to_string(); 159 | 160 | } 161 | 162 | self.last_error = None; 163 | Ok(()) 164 | } 165 | 166 | pub fn construct_sql(&mut self) -> Result { 167 | if self.last_error.is_some() { 168 | return Err(SqlParseError::new("Cannot parse Instructions, because there was an Error when Tokenizing.")); 169 | }; 170 | 171 | if self.instructions.is_empty() { 172 | return Err(SqlParseError::new("Cannot parse Instruction, because nothing is in Buffer. Did you forget to tokenize first?")); 173 | }; 174 | 175 | let mut expression: SqlExpression = SqlExpression::default(); 176 | 177 | while let Some(instruction) = self.instructions.pop_front() { 178 | if let Some(parse_error) = match instruction.to_sql(self, expression) { 179 | Ok(new_expression) => { 180 | expression = new_expression; 181 | None 182 | }, 183 | Err(parse_error) => { 184 | expression = SqlExpression::default(); 185 | Some(parse_error) 186 | } 187 | } { 188 | return Err(parse_error); 189 | } 190 | }; 191 | 192 | expression.flush(); 193 | Ok(expression.get_final().clone()) 194 | } 195 | } 196 | 197 | impl Default for AdapterInterpreter { 198 | fn default() -> Self { 199 | AdapterInterpreter { 200 | instructions: VecDeque::new(), 201 | variables: HashMap::new(), 202 | can_have_title_contains: true, 203 | can_have_descr_contains: true, 204 | can_have_due_in_days: true, 205 | last_error: None 206 | } 207 | } 208 | } 209 | 210 | impl Display for AdapterInterpreter { 211 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 212 | 213 | let result = self.instructions 214 | .iter() 215 | .map(|instr| instr 216 | .to_string()) 217 | .collect::>() 218 | .join("\n") 219 | .trim() 220 | .to_string(); 221 | 222 | write!(f, "{}", result) 223 | } 224 | } -------------------------------------------------------------------------------- /adapters/src/local_ticket_adapter/interpreter_errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{ 2 | Display, 3 | Debug, 4 | Formatter 5 | }; 6 | 7 | use std::fmt::Result as ErrorResult; 8 | 9 | #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Clone)] 10 | pub struct TokenizationError { 11 | error_string: String 12 | } 13 | 14 | impl TokenizationError { 15 | pub fn message(&self) -> String { 16 | self.error_string.clone() 17 | } 18 | } 19 | 20 | pub trait NewTokenizationError { 21 | fn new(param: T) -> Self; 22 | } 23 | 24 | impl NewTokenizationError for TokenizationError where T: ToString { 25 | fn new(param: T) -> TokenizationError { 26 | TokenizationError { error_string: param.to_string() } 27 | } 28 | } 29 | 30 | impl Display for TokenizationError { 31 | fn fmt(&self, f: &mut Formatter) -> ErrorResult { 32 | write!(f, "An Error Occurred; {}.", self.error_string) 33 | } 34 | } 35 | 36 | impl Debug for TokenizationError { 37 | fn fmt(&self, f: &mut Formatter) -> ErrorResult { 38 | let (file, line) = (file!(), line!()); 39 | write!(f, "{{ file: {file}, line: {line}, message: {} }}", self.error_string) 40 | } 41 | } 42 | 43 | #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Clone)] 44 | pub struct SqlParseError { 45 | error_string: String 46 | } 47 | 48 | impl SqlParseError { 49 | pub fn _message(&self) -> String { 50 | self.error_string.clone() 51 | } 52 | } 53 | 54 | pub trait NewSqlParseError { 55 | fn new(param: T) -> Self; 56 | } 57 | 58 | impl NewSqlParseError for SqlParseError where T: ToString { 59 | fn new(param: T) -> SqlParseError { 60 | SqlParseError { error_string: param.to_string() } 61 | } 62 | } 63 | 64 | impl Display for SqlParseError { 65 | fn fmt(&self, f: &mut Formatter) -> ErrorResult { 66 | write!(f, "An Error Occurred; {}.", self.error_string) 67 | } 68 | } 69 | 70 | impl Debug for SqlParseError { 71 | fn fmt(&self, f: &mut Formatter) -> ErrorResult { 72 | let (file, line) = (file!(), line!()); 73 | write!(f, "{{ file: {file}, line: {line}, message: {} }}", self.error_string) 74 | } 75 | } -------------------------------------------------------------------------------- /adapters/src/local_ticket_adapter/interpreter_parameters.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | interpreter::AdapterInterpreter, 3 | interpreter_errors::{TokenizationError, NewTokenizationError} 4 | }; 5 | 6 | 7 | 8 | pub enum VerifiableDataType { 9 | Text, 10 | TextArray, 11 | Number, 12 | //Boolean 13 | } 14 | 15 | impl VerifiableDataType { 16 | pub fn get_type_name(&self) -> String { 17 | match self { 18 | VerifiableDataType::Text => "Text", 19 | VerifiableDataType::TextArray => "TextArray", 20 | VerifiableDataType::Number => "Number", 21 | //VerifiableDataType::Boolean => "Boolean", 22 | }.to_string() 23 | } 24 | } 25 | 26 | pub trait VerifiableData: Sized { 27 | fn to_string(&self) -> String; 28 | fn try_tokenize(interpreter: &mut AdapterInterpreter, code: String) -> Result<(Self, String), TokenizationError>; 29 | 30 | fn is_valid_type(&self, interpreter: &AdapterInterpreter, v_type: VerifiableDataType) -> bool { 31 | match v_type { 32 | VerifiableDataType::Text => self.get_text(interpreter).is_some(), 33 | VerifiableDataType::TextArray => self.get_text_array(interpreter).is_some(), 34 | VerifiableDataType::Number => self.get_number(interpreter).is_some(), 35 | //VerifiableDataType::Boolean => self.get_boolean(interpreter).is_some(), 36 | } 37 | } 38 | 39 | fn get_text(&self, interpreter: &AdapterInterpreter) -> Option; 40 | fn get_text_array(&self, interpreter: &AdapterInterpreter) -> Option>; 41 | fn get_number(&self, interpreter: &AdapterInterpreter) -> Option; 42 | fn get_boolean(&self, interpreter: &AdapterInterpreter) -> Option; 43 | } 44 | 45 | 46 | #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Debug)] 47 | pub struct Variable { 48 | variable_name: String 49 | } 50 | 51 | impl Variable { 52 | #[cfg(test)] // This is only used in tests so far 53 | pub fn new (variable_name: String) -> Variable { 54 | Variable { variable_name } 55 | } 56 | } 57 | 58 | impl VerifiableData for Variable { 59 | 60 | fn to_string(&self) -> String { 61 | ["(::", self.variable_name.as_str(), ") "].join("") 62 | } 63 | 64 | fn try_tokenize(interpreter: &mut AdapterInterpreter, code: String) -> Result<(Self, String), TokenizationError> { 65 | 66 | let mut code_internal = code.trim_start(); 67 | if code_internal.starts_with("(::") { 68 | 69 | code_internal = code_internal.split_at(3).1; 70 | 71 | if let Some(pos) = code_internal.find(')') { 72 | let (variable, split_off_code) = code_internal.split_at(pos); 73 | code_internal = split_off_code.split_at(1).1; 74 | 75 | let final_variable = variable.trim().to_string(); 76 | 77 | if interpreter.has_variable(&final_variable) { 78 | Ok((Variable{variable_name: final_variable}, code_internal.to_string())) 79 | } else { 80 | Err(TokenizationError::new([ 81 | "::", 82 | final_variable.as_str(), 83 | " is not known." 84 | ].join("") )) 85 | } 86 | 87 | } else { 88 | Err(TokenizationError::new("Expected ) for Variable")) 89 | } 90 | 91 | } else { 92 | Err(TokenizationError::new("Expected (:: for Variable")) 93 | } 94 | } 95 | 96 | fn get_text(&self, interpreter: &AdapterInterpreter) -> Option { 97 | interpreter.get_variable(&self.variable_name).map(|found_var| found_var.to_owned()) 98 | } 99 | 100 | fn get_text_array(&self, interpreter: &AdapterInterpreter) -> Option> { 101 | interpreter.get_variable(&self.variable_name) 102 | .map(|found_var| found_var 103 | .split(',') 104 | .map(|l| l.trim().to_string()) 105 | .collect()) 106 | } 107 | 108 | fn get_number(&self, interpreter: &AdapterInterpreter) -> Option { 109 | match interpreter.get_variable(&self.variable_name) { 110 | Some(found_var) => { 111 | match found_var.as_str().parse::() { 112 | Ok(num) => Some(num), 113 | Err(_) => None 114 | } 115 | }, 116 | None => None 117 | } 118 | 119 | } 120 | 121 | fn get_boolean(&self, interpreter: &AdapterInterpreter) -> Option { 122 | match interpreter.get_variable(&self.variable_name) { 123 | Some(found_var) => { 124 | match found_var.trim().to_lowercase().as_str() { 125 | "yes" => Some(true), 126 | "no" => Some(false), 127 | "true" => Some(true), 128 | "false" => Some(false), 129 | "1" => Some(true), 130 | "0" => Some(false), 131 | _ => None 132 | } 133 | }, 134 | None => None 135 | } 136 | } 137 | } 138 | 139 | #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Debug)] 140 | pub struct Literal { 141 | literal: String 142 | } 143 | 144 | impl Literal { 145 | #[cfg(test)] 146 | pub fn new(literal: String) -> Literal { 147 | Literal { literal } 148 | } 149 | } 150 | 151 | impl VerifiableData for Literal { 152 | 153 | fn to_string(&self) -> String { 154 | ["(", self.literal.as_str(), ") "].join("") 155 | } 156 | 157 | fn try_tokenize(_interpreter: &mut AdapterInterpreter, code: String) -> Result<(Self, String), TokenizationError> { 158 | 159 | let mut code_internal = code.trim_start(); 160 | if code_internal.starts_with('(') { 161 | 162 | if code_internal.starts_with("(::") { 163 | return Err(TokenizationError::new("Not allowed to interpret :: as Literal")); 164 | }; 165 | 166 | code_internal = code_internal.split_at(1).1; 167 | 168 | if let Some(pos) = code_internal.find(')') { 169 | 170 | let (literal, split_off_code) = code_internal.split_at(pos); 171 | code_internal = split_off_code.split_at(1).1; 172 | let final_literal = literal.trim().to_string(); 173 | 174 | if !final_literal.is_empty() { 175 | Ok((Literal{literal: final_literal}, code_internal.to_string())) 176 | } else { 177 | Err(TokenizationError::new("(...) cannot be empty!")) 178 | } 179 | 180 | } else { 181 | Err(TokenizationError::new("Expected ) for Literal")) 182 | } 183 | 184 | } else { 185 | Err(TokenizationError::new("Expected ( for Literal")) 186 | } 187 | } 188 | 189 | fn get_text(&self, _interpreter: &AdapterInterpreter) -> Option { 190 | Some(self.literal.to_owned()) 191 | } 192 | 193 | fn get_text_array(&self, _interpreter: &AdapterInterpreter) -> Option> { 194 | Some(self.literal 195 | .split(',') 196 | .map(|l| l.trim().to_string()) 197 | .collect()) 198 | } 199 | 200 | fn get_number(&self, _interpreter: &AdapterInterpreter) -> Option { 201 | match self.literal.as_str().parse::() { 202 | Ok(num) => Some(num), 203 | Err(_) => None 204 | } 205 | } 206 | 207 | fn get_boolean(&self, _interpreter: &AdapterInterpreter) -> Option { 208 | match self.literal.trim().to_lowercase().as_str() { 209 | "yes" => Some(true), 210 | "no" => Some(false), 211 | "true" => Some(true), 212 | "false" => Some(false), 213 | "1" => Some(true), 214 | "0" => Some(false), 215 | _ => None 216 | } 217 | } 218 | } 219 | 220 | #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Debug)] 221 | pub enum Parameter { 222 | Variable(Variable), 223 | Literal(Literal) 224 | } 225 | 226 | impl VerifiableData for Parameter { 227 | 228 | fn to_string(&self) -> String { 229 | match self { 230 | Parameter::Variable(variable) => variable.to_string(), 231 | Parameter::Literal(literal) => literal.to_string(), 232 | } 233 | } 234 | 235 | fn try_tokenize(interpreter: &mut AdapterInterpreter, code: String) -> Result<(Self, String), TokenizationError> { 236 | let mut results: Vec> = vec![]; 237 | let mut error_messages: Vec = vec![]; 238 | 239 | match Variable::try_tokenize(interpreter, code.clone()) { 240 | Ok(result) => results.push(Ok((Parameter::Variable(result.0), result.1))), 241 | Err(err) => results.push(Err(err)), 242 | }; 243 | 244 | match Literal::try_tokenize(interpreter, code) { 245 | Ok(result) => results.push(Ok((Parameter::Literal(result.0), result.1))), 246 | Err(err) => results.push(Err(err)), 247 | }; 248 | 249 | for result in results { 250 | if let Some(result) = match result { 251 | Ok(result) => Some(result), 252 | Err(error) => { 253 | error_messages.push(error.message()); 254 | None 255 | }, 256 | } { 257 | return Ok(result); 258 | } 259 | }; 260 | let con_error_messages: Vec<&str> = error_messages.iter().map(|msg| msg.as_str()).collect(); 261 | Err(TokenizationError::new(con_error_messages.join(" or\n"))) 262 | } 263 | 264 | fn get_text(&self, interpreter: &AdapterInterpreter) -> Option { 265 | match self { 266 | Parameter::Variable(variable) => variable.get_text(interpreter), 267 | Parameter::Literal(literal) => literal.get_text(interpreter) 268 | } 269 | } 270 | 271 | fn get_text_array(&self, interpreter: &AdapterInterpreter) -> Option> { 272 | match self { 273 | Parameter::Variable(variable) => variable.get_text_array(interpreter), 274 | Parameter::Literal(literal) => literal.get_text_array(interpreter) 275 | } 276 | } 277 | 278 | fn get_number(&self, interpreter: &AdapterInterpreter) -> Option { 279 | match self { 280 | Parameter::Variable(variable) => variable.get_number(interpreter), 281 | Parameter::Literal(literal) => literal.get_number(interpreter) 282 | } 283 | } 284 | 285 | fn get_boolean(&self, interpreter: &AdapterInterpreter) -> Option { 286 | match self { 287 | Parameter::Variable(variable) => variable.get_boolean(interpreter), 288 | Parameter::Literal(literal) => literal.get_boolean(interpreter) 289 | } 290 | } 291 | } -------------------------------------------------------------------------------- /adapters/src/local_ticket_adapter/interpreter_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::local_ticket_adapter::{ 4 | interpreter::{ 5 | AdapterInterpreter, 6 | TokenizationError, 7 | NewTokenizationError 8 | }, 9 | interpreter_instructions::{ 10 | WithStateInstruction, 11 | WithTagInstruction, 12 | InBucketInstruction, 13 | AssignedToInstruction, 14 | DescriptionContainsInstruction, 15 | DueInDaysInstruction, 16 | VerifiableInstruction 17 | }, 18 | interpreter_parameters::{ 19 | Parameter as Param, 20 | Variable as Var, 21 | Literal as Lit, 22 | VerifiableData 23 | }}; 24 | 25 | #[test] 26 | fn test_interpreter_to_sql() { 27 | let mut interpreter: AdapterInterpreter = AdapterInterpreter::default(); 28 | interpreter.set_variable("me", "biochemist"); 29 | let _ = interpreter.try_tokenize([ 30 | "in_bucket(default.bucket)", 31 | "with_tag(documentation);;", 32 | "in_bucket(empty.bucket)", 33 | "with_tag(documentation)" 34 | ].join("\n")); 35 | 36 | assert_eq!(interpreter.get_last_error(), None); 37 | 38 | let result = interpreter.construct_sql(); 39 | 40 | assert_eq!(result.unwrap(), [ 41 | 42 | "SELECT tickets.* ", 43 | "FROM ((tickets) ", 44 | "JOIN buckets ON tickets.bucket_id = buckets.id) ", 45 | "JOIN ticket_tags ON tickets.id = ticket_tags.ticket_id ", 46 | "WHERE buckets.name = 'default.bucket' ", 47 | "AND ticket_tags.tag_name = 'documentation' ", 48 | "UNION SELECT tickets.* ", 49 | "FROM ((tickets) ", 50 | "JOIN buckets ON tickets.bucket_id = buckets.id) ", 51 | "JOIN ticket_tags ON tickets.id = ticket_tags.ticket_id ", 52 | "WHERE buckets.name = 'empty.bucket' ", 53 | "AND ticket_tags.tag_name = 'documentation';" 54 | 55 | ].join("")); 56 | } 57 | 58 | #[test] 59 | fn test_tokenizer_ingest() { 60 | let mut interpreter: AdapterInterpreter = AdapterInterpreter::default(); 61 | interpreter.set_variable("me", "biochemist"); 62 | let _ = interpreter.try_tokenize([ 63 | "in_bucket(default.bucket)", 64 | "with_tag(documentation);;", 65 | "in_bucket(empty.bucket)", 66 | "with_tag(documentation)" 67 | ].join("\n")); 68 | 69 | assert_eq!(interpreter.get_last_error(), None); 70 | assert_eq!(interpreter.to_string(), [ 71 | "in_bucket(default.bucket)", 72 | "with_tag(documentation)", 73 | ";;", 74 | "in_bucket(empty.bucket)", 75 | "with_tag(documentation)" 76 | ].join(" \n")); 77 | } 78 | 79 | #[test] 80 | fn test_single_tokens() { 81 | let mut interpreter: AdapterInterpreter = AdapterInterpreter::default(); 82 | interpreter.set_variable("me", "biochemist"); 83 | interpreter.set_variable("number", "42"); 84 | 85 | // An Empty String on Token level should create an Error 86 | assert_eq!( 87 | WithStateInstruction::try_tokenize(&mut interpreter, "".to_string()).err(), 88 | Some(TokenizationError::new("Expected with_state for Token".to_string()))); 89 | 90 | // Since the expression is incomplete, it will complain about a missing Variable/Literal 91 | assert_eq!( 92 | WithTagInstruction::try_tokenize(&mut interpreter, "with_tag".to_string()).err(), 93 | Some(TokenizationError::new("Expected (:: for Variable or\nExpected ( for Literal"))); 94 | 95 | // Here it will complain about the variable not existing 96 | assert_eq!( 97 | InBucketInstruction::try_tokenize(&mut interpreter, "in_bucket(::nonexistent)".to_string()).err(), 98 | Some(TokenizationError::new("::nonexistent is not known. or\nNot allowed to interpret :: as Literal"))); 99 | 100 | // This should work, since ::me exists and the syntax is correct 101 | assert_eq!( 102 | AssignedToInstruction::try_tokenize(&mut interpreter, "assigned_to(::me)".to_string()).ok(), 103 | Some((AssignedToInstruction{user: Param::Variable(Var::new("me".to_string()))}, "".to_string()))); 104 | 105 | // This will work, because the literal is valid and the syntax is correct. Also there was no description_contains before 106 | assert_eq!( 107 | DescriptionContainsInstruction::try_tokenize(&mut interpreter, "description_contains(this is part of the text)".to_string()).ok(), 108 | Some((DescriptionContainsInstruction::new( 109 | Param::Literal(Lit::new("this is part of the text".to_string()))), "".to_string()))); 110 | 111 | // This is supposed to fail, because it would be the second description_contains 112 | assert_eq!( 113 | DescriptionContainsInstruction::try_tokenize(&mut interpreter, "description_contains(this is another part of text)".to_string()).err(), 114 | Some(TokenizationError::new("Can't have more than one description_contains".to_string()))); 115 | 116 | // This is supposed to fail because due_in_days requires a number 117 | assert_eq!( 118 | DueInDaysInstruction::try_tokenize(&mut interpreter, "due_in_days(not a number)".to_string()).err(), 119 | Some(TokenizationError::new("Type of Number required for due_in_days".to_string()))); 120 | 121 | // This is supposed to work and return the variable 122 | assert_eq!( 123 | DueInDaysInstruction::try_tokenize(&mut interpreter, "due_in_days(::number)".to_string()).ok(), 124 | Some((DueInDaysInstruction{ 125 | days: Param::Variable(Var::new("number".to_string()))}, "".to_string()))); 126 | 127 | interpreter.can_have_due_in_days = true; 128 | 129 | // This is supposed to work and return the concrete value 42 130 | assert_eq!( 131 | DueInDaysInstruction::try_tokenize( 132 | &mut interpreter, 133 | "due_in_days(::number)".to_string()) 134 | .unwrap().0.days.get_number(&interpreter).unwrap(), 135 | 42); 136 | 137 | // This should return a String Vector 138 | assert_eq!( 139 | AssignedToInstruction::try_tokenize( 140 | &mut interpreter, 141 | "assigned_to(biochemic, user1, user2)".to_string()) 142 | .unwrap().0.user.get_text_array(&interpreter).unwrap(), 143 | vec!["biochemic".to_string(), "user1".to_string(), "user2".to_string()]); 144 | 145 | } 146 | } -------------------------------------------------------------------------------- /adapters/src/local_ticket_adapter/mod.rs: -------------------------------------------------------------------------------- 1 | mod adapter; 2 | mod interpreter; 3 | mod interpreter_errors; 4 | mod interpreter_tests; 5 | mod interpreter_instructions; 6 | mod interpreter_parameters; 7 | 8 | use std::sync::{ 9 | Arc, Mutex 10 | }; 11 | 12 | use tickets_rs_core::{ 13 | LocalDatabase, 14 | AppConfig, 15 | Bucket, 16 | Tag, 17 | State, 18 | Filter, 19 | FilterType, 20 | Ticket}; 21 | 22 | use tickets_rs_core::TicketAdapter; 23 | 24 | pub struct LocalTicketAdapter { 25 | database: Arc>, 26 | config: Arc>, 27 | name: String, 28 | display_name: String 29 | } 30 | 31 | impl LocalTicketAdapter { 32 | 33 | pub(crate) fn prepare_database(&self, create_default_data: bool) { 34 | let ( 35 | bucket_tables, 36 | ticket_tables, 37 | state_tables, 38 | tag_tables, 39 | tagticket_tables, 40 | filter_tables 41 | 42 | ) = match self.database.lock() { 43 | Ok(mut lock) => { 44 | 45 | let buckets = lock.create_table( 46 | &String::from("buckets"), vec![ 47 | String::from("id INTEGER PRIMARY KEY AUTOINCREMENT"), 48 | String::from("name TEXT NOT NULL"), 49 | String::from("last_change INTEGER")]); 50 | 51 | let tickets = lock.create_table( 52 | &String::from("tickets"), vec![ 53 | String::from("id INTEGER PRIMARY KEY AUTOINCREMENT"), 54 | String::from("bucket_id INTEGER NOT NULL"), 55 | String::from("title TEXT NOT NULL"), 56 | String::from("state_name TEXT NOT NULL"), 57 | String::from("description TEXT"), 58 | String::from("created_at INTEGER"), 59 | String::from("due_at INTEGER"), 60 | String::from("assigned_to TEXT")]); 61 | 62 | let ticket_tags = lock.create_table( 63 | &String::from("ticket_tags"), vec![ 64 | String::from("ticket_id INTEGER NOT NULL"), 65 | String::from("tag_name TEXT NOT NULL")]); 66 | 67 | let states = lock.create_table( 68 | &String::from("states"), vec![ 69 | String::from("name TEXT NOT NULL PRIMARY KEY"), 70 | String::from("description TEXT"), 71 | String::from("sorting_order INTEGER NOT NULL")]); 72 | 73 | let tags = lock.create_table( 74 | &String::from("tags"), vec![ 75 | String::from("name TEXT NOT NULL PRIMARY KEY"), 76 | String::from("color TEXT NOT NULL"), 77 | String::from("color_text TEXT NOT NULL")]); 78 | 79 | let filters = lock.create_table( 80 | &String::from("filters"), vec![ 81 | String::from("name TEXT NOT NULL PRIMARY KEY"), 82 | String::from("operation TEXT NOT NULL")]); 83 | 84 | (buckets, tickets, states, tags, ticket_tags, filters) 85 | }, 86 | Err(_) => (false, false, false, false, false, false), 87 | }; 88 | 89 | if create_default_data { 90 | let mut bucket_default = Bucket::default() 91 | .with_adapter(self) 92 | .with_details(0, String::from("default.bucket")); 93 | 94 | let mut bucket_empty = Bucket::default() 95 | .with_adapter(self) 96 | .with_details(0, String::from("empty.bucket")); 97 | 98 | if bucket_tables { 99 | 100 | match self.bucket_write(&mut bucket_default) 101 | .and(self.bucket_write(&mut bucket_empty)) { 102 | Ok(_) => (), 103 | Err(err) => println!("Wasn't able to write buckets as default data due to {err}"), 104 | }; 105 | } 106 | 107 | let tag_bug = Tag::default() 108 | .with_adapter(self) 109 | .with_name(String::from("bug")) 110 | .with_hex_colors("#321820", "#b87881"); 111 | 112 | let tag_enhancement = Tag::default() 113 | .with_adapter(self) 114 | .with_name(String::from("enhancement")) 115 | .with_hex_colors("#28393e", "#8fd4d5"); 116 | 117 | let tag_documentation = Tag::default() 118 | .with_adapter(self) 119 | .with_name(String::from("documentation")) 120 | .with_hex_colors("#0b2337", "#309ce8"); 121 | 122 | let tag_wontfix = Tag::default() 123 | .with_adapter(self) 124 | .with_name(String::from("wontfix")) 125 | .with_hex_colors("#393c41", "#d7d7d7"); 126 | 127 | let tag_blocker = Tag::default() 128 | .with_adapter(self) 129 | .with_name(String::from("blocker")) 130 | .with_hex_colors("#a71a35", "#ffd9df"); 131 | 132 | let tag_high_prio = Tag::default() 133 | .with_adapter(self) 134 | .with_name(String::from("high priority")) 135 | .with_hex_colors("#a7581a", "#ffecd9"); 136 | 137 | let tag_low_prio = Tag::default() 138 | .with_adapter(self) 139 | .with_name(String::from("low priority")) 140 | .with_hex_colors("#4e542a", "#c9d5b6"); 141 | 142 | if tag_tables { 143 | match self.tag_write(&tag_bug) 144 | .and(self.tag_write(&tag_enhancement)) 145 | .and(self.tag_write(&tag_documentation)) 146 | .and(self.tag_write(&tag_wontfix)) 147 | .and(self.tag_write(&tag_blocker)) 148 | .and(self.tag_write(&tag_high_prio)) 149 | .and(self.tag_write(&tag_low_prio)) { 150 | Ok(_) => (), 151 | Err(err) => println!("Wasn't able to write tags as default data due to {err}"), 152 | }; 153 | }; 154 | 155 | let state_new = State::default() 156 | .with_adapter(self) 157 | .with_name(String::from("new")) 158 | .with_description(String::from(concat!("A Ticket qualifies as new, if it has been created, ", 159 | "but nobody has worked on it yet."))) 160 | .with_order(0); 161 | 162 | let state_pause = State::default() 163 | .with_adapter(self) 164 | .with_name(String::from("pause")) 165 | .with_description(String::from(concat!("A Ticket qualifies as paused, if it has been worked", 166 | " on, but the work has been paused for now."))) 167 | .with_order(0); 168 | 169 | let state_open = State::default() 170 | .with_adapter(self) 171 | .with_name(String::from("open")) 172 | .with_description(String::from("A Ticket qualifies as open, if it is actively worked on.")) 173 | .with_order(1); 174 | 175 | let state_done = State::default() 176 | .with_adapter(self) 177 | .with_name(String::from("done")) 178 | .with_description(String::from("A Ticket is done, when the content of it has been finished.")) 179 | .with_order(2); 180 | 181 | let state_live = State::default() 182 | .with_adapter(self) 183 | .with_name(String::from("live")) 184 | .with_description(String::from(concat!("Specifically on Running Systems with multiple ", 185 | "Branches it is important to not, if a ticket is live. A Ticket qualifies ", 186 | "as live, if it's feature is used in the corresponding production environment."))) 187 | .with_order(3); 188 | 189 | if state_tables { 190 | 191 | match self.state_write(&state_new) 192 | .and(self.state_write(&state_pause)) 193 | .and(self.state_write(&state_open)) 194 | .and(self.state_write(&state_done)) 195 | .and(self.state_write(&state_live)) { 196 | Ok(_) => (), 197 | Err(err) => println!("Wasn't able to write states as default data due to {err}"), 198 | } 199 | } 200 | 201 | if filter_tables { 202 | 203 | let filter_state_new = Filter::default() 204 | .with_adapter(self) 205 | .with_type(FilterType::User) 206 | .with_details( 207 | format!("{}_state_new", self.get_name()), 208 | Filter::filter_expression(self.get_name(), 209 | "with_state(new)" 210 | ) 211 | ); 212 | 213 | let filter_tag_doc = Filter::default() 214 | .with_adapter(self) 215 | .with_type(FilterType::User) 216 | .with_details( 217 | format!("{}_tag_doc", self.get_name()), 218 | Filter::filter_expression(self.get_name(), 219 | "with_tag(documentation)" 220 | ) 221 | ); 222 | 223 | let filter_assigned_to_me = Filter::default() 224 | .with_adapter(self) 225 | .with_type(FilterType::User) 226 | .with_details( 227 | format!("{}_assigned_to_me", self.get_name()), 228 | Filter::filter_expression(self.get_name(), 229 | "assigned_to(::me)" 230 | ) 231 | ); 232 | 233 | let filter_example_1 = Filter::default() 234 | .with_adapter(self) 235 | .with_type(FilterType::User) 236 | .with_details( 237 | format!("{}_example_1", self.get_name()), 238 | Filter::filter_expression(self.get_name(), 239 | "with_state(new) 240 | with_state(open)" 241 | ) 242 | ); 243 | 244 | let filter_example_2 = Filter::default() 245 | .with_adapter(self) 246 | .with_type(FilterType::User) 247 | .with_details( 248 | format!("{}_example_2", self.get_name()), 249 | Filter::filter_expression(self.get_name(), 250 | "in_bucket(default.bucket) 251 | with_tag(documentation);; 252 | in_bucket(empty.bucket) 253 | with_tag(documentation)" 254 | ) 255 | ); 256 | 257 | let filter_example_3 = Filter::default() 258 | .with_adapter(self) 259 | .with_type(FilterType::User) 260 | .with_details( 261 | format!("{}_example_3", self.get_name()), 262 | Filter::filter_expression(self.get_name(), 263 | "assigned_to(::me) 264 | due_in_days(7)" 265 | ) 266 | ); 267 | 268 | match self.filter_write(&filter_state_new) 269 | .and(self.filter_write(&filter_tag_doc)) 270 | .and(self.filter_write(&filter_assigned_to_me)) 271 | .and(self.filter_write(&filter_example_1)) 272 | .and(self.filter_write(&filter_example_2)) 273 | .and(self.filter_write(&filter_example_3)) { 274 | Ok(_) => (), 275 | Err(err) => println!("Wasn't able to write filters as default data due to {err}"), 276 | } 277 | 278 | } 279 | 280 | if ticket_tables && tagticket_tables { 281 | 282 | let ticket_example_task = self.ticket_write( 283 | &Ticket::default() 284 | .with_adapter(self) 285 | .with_bucket(&bucket_default) 286 | .with_details(0, String::from("Example Task"), 287 | String::from("This is an example Task, created to test functionality.")) 288 | .with_state(&state_new) 289 | .with_tags(vec![ 290 | &tag_low_prio, 291 | &tag_documentation, 292 | &Tag::default().with_name(String::from("example")).with_random_colors(), 293 | &Tag::default().with_name(String::from("example2")).with_random_colors(), 294 | &Tag::default().with_name(String::from("example3")).with_random_colors() 295 | ]) 296 | .with_assignee("biochemist".to_string()) 297 | ); 298 | 299 | let ticket_second_task = self.ticket_write( 300 | &Ticket::default() 301 | .with_adapter(self) 302 | .with_bucket(&bucket_default) 303 | .with_details(0, String::from("Second Task"), 304 | String::from("This is the second example Task, created to test functionality.")) 305 | .with_state(&state_open) 306 | .with_tags(vec![ 307 | &Tag::default().with_name(String::from("example")).with_random_colors(), 308 | &Tag::default().with_name(String::from("example2")).with_random_colors(), 309 | &Tag::default().with_name(String::from("example3")).with_random_colors() 310 | ]) 311 | .with_assignee("user2".to_string()) 312 | ); 313 | 314 | let ticket_long_title = self.ticket_write( 315 | &Ticket::default() 316 | .with_adapter(self) 317 | .with_bucket(&bucket_empty) 318 | .with_details(0, ["The title of this ticket is very long, ", 319 | "to test the extreme case of rendering a Ticket."].join(""), 320 | ["This is the third example Task, created to test rendering extremes. ", 321 | "That also means, that this text is very long, so you need to read a", 322 | " moment, and it also won't fit into the ticket completely. Well", 323 | ", atleast thats the plan."].join("")) 324 | .with_state(&state_open) 325 | .with_tags(vec![ 326 | &tag_blocker, 327 | &tag_bug, 328 | &tag_documentation, 329 | &Tag::default().with_name(String::from("example3")).with_random_colors() 330 | ]) 331 | ); 332 | 333 | let ticket_markdown = self.ticket_write( 334 | &Ticket::default() 335 | .with_adapter(self) 336 | .with_bucket(&bucket_empty) 337 | .with_details(0, "This ticket contains some Markdown in the Description".to_string(), 338 | ["Here's a numbered list:", 339 | "", 340 | " 1. first item", 341 | " 2. second item", 342 | " 3. third item", 343 | "", 344 | "Note again how the actual text starts at 4 columns in (4 characters", 345 | "from the left side). Here's a code sample:", 346 | "", 347 | " # Let me re-iterate ...", 348 | " for i in 1 .. 10 { do-something(i) }", 349 | "", 350 | "As you probably guessed, indented 4 spaces. By the way, instead of", 351 | "indenting the block, you can use delimited blocks, if you like:", 352 | "", 353 | "~~~", 354 | "define foobar() {", 355 | " print \"Welcome to flavor country!\";", 356 | "}", 357 | "~~~", 358 | "", 359 | "(which makes copying & pasting easier). You can optionally mark the", 360 | "delimited block for Pandoc to syntax highlight it:", 361 | "", 362 | "~~~py", 363 | "import time", 364 | "# Quick, count to ten!", 365 | "for i in range(10):", 366 | " # (but not *too* quick)", 367 | " time.sleep(0.5)", 368 | " print(i)", 369 | "~~~", 370 | "", 371 | "Here's a link to [the cargo website for testing](https://crates.io/)", 372 | "", 373 | "![image example](https://github.com/TheBiochemic/free_px_assets/blob/main/biocraft_textures/Block_Exclusive.png?raw=true \"example image\")" 374 | ].join("\n")) 375 | .with_state(&state_live) 376 | .with_tags(vec![ 377 | &tag_enhancement, 378 | &tag_documentation, 379 | &Tag::default().with_name(String::from("markdown")).with_random_colors() 380 | ]) 381 | ); 382 | 383 | match ticket_example_task 384 | .and(ticket_second_task) 385 | .and(ticket_long_title) 386 | .and(ticket_markdown) { 387 | Ok(_) => (), 388 | Err(err) => println!("Wasn't able to write tickets as default data due to {err}"), 389 | } 390 | } 391 | } 392 | } 393 | 394 | fn list_builtin_filters(&self) -> Vec { 395 | 396 | let buckets = self.bucket_list_all(); 397 | 398 | buckets.iter().map(|bucket| { 399 | Filter::default() 400 | .with_details( 401 | bucket.name.clone(), 402 | Filter::filter_expression(self.get_name(), &format!("in_bucket({})", bucket.name))) 403 | .with_type(FilterType::Bucket(bucket.identifier.id)) 404 | .with_adapter(self) 405 | }).collect::>() 406 | } 407 | 408 | } -------------------------------------------------------------------------------- /assets/adapters/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBiochemic/tickets-rs/f3ddd9001fc3d32d69ef676f78dc77d2b99013eb/assets/adapters/database.png -------------------------------------------------------------------------------- /assets/adapters/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBiochemic/tickets-rs/f3ddd9001fc3d32d69ef676f78dc77d2b99013eb/assets/adapters/github.png -------------------------------------------------------------------------------- /assets/icon_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBiochemic/tickets-rs/f3ddd9001fc3d32d69ef676f78dc77d2b99013eb/assets/icon_app.png -------------------------------------------------------------------------------- /assets/screenshot_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheBiochemic/tickets-rs/f3ddd9001fc3d32d69ef676f78dc77d2b99013eb/assets/screenshot_app.png -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | rust-version = "1.70" 3 | name = "tickets-rs-core" 4 | version = "0.1.0" 5 | authors = ["Robert Lang"] 6 | edition = "2021" 7 | description = "The Package containing the data model and some traits for tickets-rs" 8 | 9 | [dependencies] 10 | rusqlite = { version = "0.28.0", features = ["bundled"] } 11 | egui = { version = "0.23.0", features = ["color-hex"] } 12 | eframe = "0.23.0" 13 | rand = "0.8.5" -------------------------------------------------------------------------------- /core/src/adapter_base/adapter_error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{ 2 | Display, 3 | Formatter, 4 | Debug, 5 | Result 6 | }; 7 | 8 | type Location = String; 9 | type Message = String; 10 | type ErrorsVec = Vec<(Location, Message)>; 11 | 12 | #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Clone)] 13 | pub enum AdapterErrorType { 14 | TicketWrite, 15 | TicketDelete, 16 | BucketWrite, 17 | BucketDelete, 18 | TagWrite, 19 | TagDelete, 20 | StateWrite, 21 | FilterWrite, 22 | FilterDelete, 23 | Access, 24 | Validate(ErrorsVec, String), 25 | Expression(String), 26 | Instantiation 27 | } 28 | 29 | #[derive(Eq, Hash, Ord, PartialEq, PartialOrd, Clone)] 30 | pub struct AdapterError { 31 | pub error_type: AdapterErrorType 32 | } 33 | 34 | impl AdapterError { 35 | pub fn new(error_type: AdapterErrorType) -> Self { 36 | AdapterError { error_type } 37 | } 38 | 39 | pub fn get_text(&self) -> String { 40 | 41 | let mut message = String::default(); 42 | 43 | match &self.error_type { 44 | AdapterErrorType::TicketDelete => message += "Failed to delete Ticket", 45 | AdapterErrorType::TicketWrite => message += "Failed to write Ticket", 46 | AdapterErrorType::Validate(_, name) => message += ("Failed to validate ".to_owned() + name).as_str(), 47 | AdapterErrorType::BucketWrite => message += "Failed to write Bucket", 48 | AdapterErrorType::BucketDelete => message += "Failed to delete Bucket", 49 | AdapterErrorType::FilterWrite => message += "Failed to write custom Filter", 50 | AdapterErrorType::FilterDelete => message += "Failed to delete custom Filter", 51 | AdapterErrorType::TagWrite => message += "Failed to write Tag", 52 | AdapterErrorType::TagDelete => message += "Failed to delete Tag", 53 | AdapterErrorType::StateWrite => message += "Failed to write State", 54 | AdapterErrorType::Access => message += "Failed access Adapter Data", 55 | AdapterErrorType::Expression(text) => message += ("Failed to execute Expression correctly. Reason: ".to_string() + text.as_str()).as_str(), 56 | AdapterErrorType::Instantiation => message += "Failed to instantiate Adapter" 57 | } 58 | 59 | message 60 | } 61 | } 62 | 63 | impl Display for AdapterError { 64 | fn fmt(&self, f: &mut Formatter) -> Result { 65 | write!(f, "An Error Occurred; {}.", self.get_text()) 66 | } 67 | } 68 | 69 | impl Debug for AdapterError { 70 | fn fmt(&self, f: &mut Formatter) -> Result { 71 | let (file, line) = (file!(), line!()); 72 | write!(f, "{{ file: {file}, line: {line}, message: {} }}", self.get_text()) 73 | } 74 | } -------------------------------------------------------------------------------- /core/src/adapter_base/mod.rs: -------------------------------------------------------------------------------- 1 | mod adapter_error; 2 | mod ticket_adapter; 3 | 4 | pub use adapter_error::AdapterError; 5 | pub use adapter_error::AdapterErrorType; 6 | pub use ticket_adapter::TicketAdapter; -------------------------------------------------------------------------------- /core/src/adapter_base/ticket_adapter.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, sync::{Mutex, Arc}}; 2 | 3 | use crate::{ 4 | data_model::{ 5 | Bucket, 6 | Ticket, 7 | State, 8 | Tag, 9 | Filter, 10 | Config 11 | }, 12 | AppConfig 13 | }; 14 | 15 | pub use super::adapter_error::AdapterError as AdapterError; 16 | 17 | pub trait TicketAdapter { 18 | 19 | /** 20 | Returns the generic type name, that is used to identify it's type from config 21 | */ 22 | fn get_type_name() -> String where Self: Sized; 23 | 24 | /** 25 | Returns the fancy generic Type name, that is displayed before instantiating the Adapter 26 | */ 27 | fn get_fancy_type_name() -> String where Self: Sized; 28 | 29 | /** 30 | Returns the technical name of the ticket adapter. This name is 31 | used in commands and expressions 32 | */ 33 | fn get_name(&self) -> String; 34 | 35 | /** 36 | Returns a reference Configuration needed for instantiating the Ticket Adapter 37 | */ 38 | fn create_config() -> Config where Self: Sized; 39 | 40 | /** 41 | Creates an instance from Configuration 42 | */ 43 | fn from_config(app_config: Arc>, config: &Config, finished: Arc>) -> Result, AdapterError> where Self: Sized; 44 | 45 | /** 46 | Returns the path to the icon for this particular adapter. 47 | */ 48 | fn get_icon(&self) -> &Path; 49 | 50 | /** 51 | Returns the Human readable name, this is the Name, that gets 52 | displayed in the USer interface 53 | */ 54 | fn get_fancy_name(&self) -> String; 55 | 56 | /** 57 | Returns a boolean on wether the Adapter is readonly or not. 58 | If it is readonly 59 | */ 60 | fn is_read_only(&self) -> bool; 61 | 62 | /** 63 | Lists all Buckets, that are provided by this adapter 64 | If the operation fails, returns an empty vector 65 | */ 66 | fn bucket_list_all(&self) -> Vec; 67 | 68 | /** 69 | Lists a single Bucket this adapter can provide, defned by it's id. 70 | if the read fails, the function returns None 71 | */ 72 | fn bucket_list_unique(&self, id: u64) -> Option; 73 | 74 | /** 75 | Tries to delete a bucket off this adapter. If the delete fails for 76 | for whatever reason, an AdapterError is being thrown. 77 | */ 78 | fn bucket_drop(&self, bucket: &Bucket) -> Result<(), AdapterError>; 79 | 80 | /** 81 | Writes a bucket to this adapter. If the Write fails, it will return 82 | an AdapterError, otherwise an empty tuple, that can be ignored 83 | */ 84 | fn bucket_write(&self, bucket: &mut Bucket) -> Result<(), AdapterError>; 85 | 86 | /** 87 | Lists all tickets, this adapter can provide. If the read fails, or 88 | no tickets are available, an empty vector will be returned 89 | */ 90 | fn ticket_list_all(&self) -> Vec; 91 | 92 | 93 | /** 94 | Lists a single Ticket this adapter can provide, defned by it's id. 95 | if the read fails, the function returns None 96 | */ 97 | fn ticket_list_unique(&self, id: i64) -> Option; 98 | 99 | /** 100 | Lists tickets, that the adapter can provide based on an adapter 101 | specific expression. If this expression is invalid, an error will 102 | be thrown, a vector with (or without) tickets otherwise 103 | */ 104 | fn ticket_list(&self, expression: &str) -> Result, AdapterError>; 105 | 106 | /** 107 | Tries to write a ticket to this adapter. If the write fails, it 108 | throw an AdapterError. 109 | */ 110 | fn ticket_write(&self, ticket: &Ticket) -> Result<(), AdapterError>; 111 | 112 | /** 113 | Tries to delete a ticket off this adapter. If the delete fails for 114 | for whatever reason, an AdapterError is being thrown. 115 | */ 116 | fn ticket_drop(&self, ticket: &Ticket) -> Result<(), AdapterError>; 117 | 118 | /** 119 | Lists all states in a list, that are available to this adapter 120 | or an empty list 121 | */ 122 | fn state_list_all(&self) -> Vec; 123 | 124 | /** 125 | Writes a state to the adapter. if the write failed, then an 126 | AdapterError will be thrown. On Success it returns an empty 127 | Tuple 128 | */ 129 | fn state_write(&self, state: &State) -> Result<(), AdapterError>; 130 | 131 | /** 132 | Instructs the adapter to list all available tags. 133 | If any tags are available, then it will return a vector with 134 | all of them, an empty one otherwise 135 | */ 136 | fn tag_list_all(&self) -> Vec; 137 | 138 | /** 139 | Attempts to write a tag to the adapter. It the write fails, 140 | an error gets thrown. Otherwise an empty tuple gets returned 141 | */ 142 | fn tag_write(&self, state: &Tag) -> Result<(), AdapterError>; 143 | 144 | /** 145 | Tries to remove a tag from this adapter. If the delete 146 | operation fails for whatever reason, an error gets thrown. 147 | */ 148 | fn tag_drop(&self, state: &Tag) -> Result<(), AdapterError>; 149 | 150 | /** 151 | Lists all available filters in this adapter as a vector, 152 | returns an empty vector, if no filters have been found 153 | */ 154 | fn filter_list_all(&self) -> Vec; 155 | 156 | /** 157 | Get a filter from the adapter, that has the same name as the 158 | supplied string. If no Filter is found, None is returned. 159 | */ 160 | fn filter_list(&self, filter_name: String) -> Option; 161 | 162 | /** 163 | Attempts to write a filter to the adapter. If there is an Error, 164 | a AdapterError gets thrown. 165 | */ 166 | fn filter_write(&self, filter: &Filter) -> Result<(), AdapterError>; 167 | 168 | /** 169 | Tries to delete a filter off this adapter. If the delete fails for 170 | for whatever reason, an AdapterError is being thrown. 171 | */ 172 | fn filter_drop(&self, filter: &Filter) -> Result<(), AdapterError>; 173 | 174 | /** 175 | Tests, if a filter expression is valid for this specific adapter. If there is 176 | a problem, returns a List of errors in the form of Vec<(Attribute Name, Error Message)>, 177 | otherwise returns an Empty Tuple. 178 | */ 179 | fn filter_expression_validate(&self, expression: &String) -> Result<(), Vec<(String, String)>>; 180 | } -------------------------------------------------------------------------------- /core/src/data_model/bucket.rs: -------------------------------------------------------------------------------- 1 | use std::time::{ 2 | SystemTime, 3 | UNIX_EPOCH 4 | }; 5 | 6 | use crate::TicketAdapter; 7 | 8 | #[derive(Default, PartialEq, Clone, Eq, Hash, PartialOrd, Ord, Debug)] 9 | pub struct BucketIdentifier { 10 | pub adapter: String, 11 | pub id: u64, 12 | } 13 | 14 | impl BucketIdentifier { 15 | pub fn new(adapter: &String, id: u64) -> BucketIdentifier { 16 | BucketIdentifier { 17 | adapter: adapter.clone(), 18 | id 19 | } 20 | } 21 | } 22 | 23 | #[derive(Eq, PartialOrd, Ord, Debug, PartialEq, Clone)] 24 | pub struct Bucket { 25 | pub identifier: BucketIdentifier, 26 | pub name: String, 27 | pub last_change: i64 28 | } 29 | 30 | impl Default for Bucket { 31 | 32 | fn default() -> Self { 33 | Bucket{ 34 | identifier: BucketIdentifier::default(), 35 | name: String::default(), 36 | last_change: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 37 | } 38 | } 39 | } 40 | 41 | impl Bucket { 42 | 43 | pub fn with_adapter(mut self, adapter: &dyn TicketAdapter) -> Self { 44 | self.identifier.adapter = adapter.get_name(); 45 | self 46 | } 47 | 48 | pub fn with_details(mut self, id: u64, name: String) -> Self { 49 | self.identifier.id = id; 50 | self.name = name; 51 | self 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /core/src/data_model/bucket_panel_location.rs: -------------------------------------------------------------------------------- 1 | 2 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone)] 3 | pub enum BucketPanelLocationType { 4 | All, 5 | Reset, 6 | Filter, 7 | Adapter, 8 | Entry 9 | } 10 | 11 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] 12 | pub struct BucketPanelLocation { 13 | pub entry_type: BucketPanelLocationType, 14 | pub adapter: String, 15 | pub section: String 16 | } -------------------------------------------------------------------------------- /core/src/data_model/config/app_config.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | Mutex, 3 | Arc 4 | }; 5 | 6 | use rusqlite::types::Value; 7 | use std::str; 8 | 9 | use crate::LocalDatabase; 10 | 11 | use super::{ConfigOption, Config}; 12 | use super::config_option::ToConfig; 13 | 14 | 15 | pub struct AppConfig { 16 | database: Arc>, 17 | config: Config 18 | } 19 | 20 | impl Drop for AppConfig { 21 | fn drop(&mut self) { 22 | 23 | match self.database.lock() { 24 | Ok(db_lock) => { 25 | match db_lock.connection.lock() { 26 | Ok(lock) => { 27 | 28 | // Build dynamic expression 29 | let mut expression: Vec<&str> = vec![]; 30 | let mut parameters: Vec = vec![]; 31 | 32 | //remove all old content 33 | { 34 | let mut statement = lock.prepare("DELETE FROM config").unwrap(); 35 | 36 | if let Err(err) = statement.execute([]) { 37 | println!("There was an error executing this first writing operation! Reason: {}", err); 38 | } 39 | } 40 | 41 | if self.config.is_empty() { 42 | return; 43 | }; 44 | 45 | //add new and updated entries 46 | expression.push("INSERT INTO config"); 47 | expression.push("(name, value, display_options)"); 48 | expression.push("VALUES"); 49 | 50 | let options_vec = Vec::from_iter(self.config.iter()); 51 | if let Some((last_option, options)) = options_vec.split_last() { 52 | for option in options { 53 | expression.push("(?, ?, ?),"); 54 | parameters.push(Value::Text(option.0.to_string())); 55 | parameters.push(Value::Text(option.1.value.clone())); 56 | parameters.push(Value::Text(option.1.display_options.clone())); 57 | } 58 | 59 | expression.push("(?, ?, ?)"); 60 | parameters.push(Value::Text(last_option.0.to_string())); 61 | parameters.push(Value::Text(last_option.1.value.clone())); 62 | parameters.push(Value::Text(last_option.1.display_options.clone())); 63 | } 64 | 65 | // println!("{:?}", expression); 66 | // println!("{:?}", parameters); 67 | 68 | // Finally, execute query, that deletes old data, and updates config options 69 | let mut statement = lock.prepare(expression.join(" ").as_str()).unwrap(); 70 | 71 | let execute_params = rusqlite::params_from_iter(parameters.iter()); 72 | if let Err(err) = statement.execute(execute_params) { 73 | println!("There was an error executing this writing operation! Reason: {}", err); 74 | }; 75 | 76 | }, 77 | Err(e) => { 78 | println!("Wasn't able to lock Connection for writing Config, {}", e); 79 | } 80 | } 81 | }, 82 | Err(e) => { 83 | println!("Wasn't able to lock Database for writing Config, {}", e); 84 | } 85 | } 86 | 87 | } 88 | } 89 | 90 | 91 | impl AppConfig { 92 | 93 | pub fn new(database: Arc>) -> Self { 94 | 95 | let mut config = Config::default(); 96 | 97 | //Lock the Database 98 | match database.lock() { 99 | Ok(mut lock) => { 100 | 101 | //create the table, of possible 102 | if lock.create_table( 103 | &String::from("config"), vec![ 104 | String::from("name TEXT NOT NULL PRIMARY KEY"), 105 | String::from("value TEXT NOT NULL"), 106 | String::from("display_options TEXT NOT NULL")]) { 107 | 108 | config.put("username", "new User", ""); 109 | } 110 | 111 | //Load all entries into RAM 112 | match lock.connection.lock() { 113 | Ok(conn_lock) => { 114 | 115 | let expression = String::from("SELECT * FROM config"); 116 | let mut statement = conn_lock.prepare(expression.as_str()).unwrap(); 117 | 118 | let mut rows = statement.query([]).unwrap(); 119 | 120 | while let Some(row) = rows.next().unwrap() { 121 | 122 | let name: String = row.get(0).unwrap(); 123 | let value: String = row.get(1).unwrap(); 124 | let display_options: String = row.get(2).unwrap(); 125 | 126 | config.put(name.as_str(), value, display_options.as_str()); 127 | } 128 | 129 | }, 130 | Err(_) => println!("Wasn't able to lock connection within Database for Config") 131 | } 132 | }, 133 | Err(err) => println!("Wasn't able to lock Database for preparing config. Reason: {}", err) 134 | } 135 | 136 | 137 | AppConfig { 138 | database, 139 | config 140 | } 141 | } 142 | 143 | pub fn get(&self, name: &str) -> Option<&ConfigOption> { 144 | self.config.get(name) 145 | } 146 | 147 | pub fn get_or_default(&mut self, name: &str, value: T, display_options: &str) -> &ConfigOption { 148 | self.config.get_or_default(name, value, display_options) 149 | } 150 | 151 | pub fn put(&mut self, name: &str, value: T, display_options: &str) { 152 | self.config.put(name, value, display_options) 153 | } 154 | 155 | pub fn get_sub_config(&self, prefix: &str) -> Config { 156 | self.config.get_sub_config(prefix) 157 | } 158 | 159 | pub fn put_sub_config(&mut self, other: &Config, prefix: &str) { 160 | self.config.put_sub_config(other, prefix); 161 | } 162 | 163 | pub fn drop_entry(&mut self, entry_name: &str) -> bool { 164 | self.config.drop_entry(entry_name) 165 | } 166 | 167 | pub fn drop_sub_config(&mut self, prefix: &str) -> u32 { 168 | self.config.drop_sub_config(prefix) 169 | } 170 | } -------------------------------------------------------------------------------- /core/src/data_model/config/config_base.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use super::{config_option::ToConfig, ConfigOption}; 4 | 5 | #[derive(Default, PartialEq, Clone)] 6 | pub struct Config { 7 | options: BTreeMap, 8 | } 9 | 10 | impl Config { 11 | pub fn len(&self) -> usize { 12 | self.options.len() 13 | } 14 | 15 | pub fn is_empty(&self) -> bool { 16 | self.options.is_empty() 17 | } 18 | 19 | pub fn iter(&self) -> impl Iterator { 20 | self.options.iter() 21 | } 22 | 23 | pub fn get(&self, name: &str) -> Option<&ConfigOption> { 24 | self.options.get(&name.to_string()) 25 | } 26 | 27 | pub fn get_or_default(&mut self, name: &str, value: T, display_options: &str) -> &ConfigOption { 28 | 29 | let name_string = name.to_string(); 30 | 31 | if !self.options.contains_key(&name_string) { 32 | self.put(name, value, display_options); 33 | } 34 | 35 | self.options.get(name).unwrap() 36 | } 37 | 38 | pub fn put(&mut self, name: &str, value: T, display_options: &str) { 39 | self.options.insert( 40 | name.to_string(), 41 | ConfigOption { 42 | value: value.to_config(), 43 | display_options: display_options.to_string(), 44 | }); 45 | } 46 | 47 | pub fn drop_entry(&mut self, name: &str) -> bool { 48 | let name_string = name.to_string(); 49 | let contains = self.options.contains_key(&name_string); 50 | if contains { 51 | self.options.remove(&name_string); 52 | true 53 | } else { 54 | false 55 | } 56 | } 57 | 58 | pub fn drop_sub_config(&mut self, prefix: &str) -> u32 { 59 | let mut found_names: Vec = Vec::default(); 60 | let prefix_string = prefix.to_string() + ":"; 61 | let prefix_corrected = prefix_string.as_str(); 62 | let mut removed_elements: u32 = 0; 63 | 64 | for entry in self.options.keys() { 65 | if entry.starts_with(prefix_corrected) { 66 | found_names.push(entry.clone()) 67 | } 68 | } 69 | 70 | for to_remove in found_names { 71 | if self.options.remove(&to_remove).is_some() { 72 | removed_elements += 1; 73 | }; 74 | } 75 | 76 | removed_elements 77 | } 78 | 79 | pub fn with(mut self, name: &str, value: T, display_options: &str) -> Self { 80 | self.put(name, value, display_options); 81 | self 82 | } 83 | 84 | pub fn get_sub_config(&self, prefix: &str) -> Config { 85 | let mut sub_config = Config::default(); 86 | 87 | for entry in &self.options { 88 | if entry.0.starts_with((prefix.to_string() + ":").as_str()) { 89 | 90 | let mut entry_name = entry.0.clone(); 91 | entry_name = entry_name[prefix.len() + 1 .. entry_name.len()].to_string(); 92 | 93 | sub_config.put(entry_name.as_str(), entry.1.value.clone(), &entry.1.display_options) 94 | } 95 | }; 96 | 97 | sub_config 98 | } 99 | 100 | pub fn put_sub_config(&mut self, other: &Config, prefix: &str) { 101 | 102 | for entry in &other.options { 103 | let entry_name = prefix.to_string() + ":" + entry.0; 104 | self.put(entry_name.as_str(), entry.1.value.as_str(), entry.1.display_options.as_str()); 105 | } 106 | 107 | } 108 | } -------------------------------------------------------------------------------- /core/src/data_model/config/config_option.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use eframe::egui::Color32; 3 | use std::fmt::Write; 4 | use std::str; 5 | 6 | #[derive(Default, PartialEq, Clone)] 7 | pub struct ConfigOption { 8 | pub(super) value: String, 9 | pub(super) display_options: String 10 | } 11 | 12 | pub trait ToConfig { 13 | fn to_config(&self) -> String; 14 | fn to_self(value: &str) -> Option where Self: Sized; 15 | } 16 | 17 | impl ToConfig for String { 18 | fn to_config(&self) -> String { 19 | self.to_string() 20 | } 21 | 22 | fn to_self(value: &str) -> Option { 23 | Some(value.to_string()) 24 | } 25 | } 26 | 27 | impl ToConfig for &str { 28 | fn to_config(&self) -> String { 29 | self.to_string() 30 | } 31 | 32 | fn to_self(_value: &str) -> Option { 33 | None 34 | } 35 | } 36 | 37 | impl ToConfig for i32 { 38 | fn to_config(&self) -> String { 39 | self.to_string() 40 | } 41 | 42 | fn to_self(value: &str) -> Option { 43 | match i32::from_str(value) { 44 | Ok(int) => Some(int), 45 | Err(_) => None, 46 | } 47 | } 48 | } 49 | 50 | impl ToConfig for Color32 { 51 | fn to_config(&self) -> String { 52 | let mut col_string = String::from("#"); 53 | for byte in self.to_array() { 54 | write!(&mut col_string, "{:02X}", byte).expect("Unable to write string for converting color!"); 55 | } 56 | 57 | col_string 58 | } 59 | 60 | fn to_self(value: &str) -> Option { 61 | let subs = value[1..].as_bytes() 62 | .chunks(2) 63 | .map( |unmapped| u8::from_str_radix(str::from_utf8(unmapped).unwrap(), 16)) 64 | .collect::, _>>() 65 | .unwrap(); 66 | 67 | if subs.len() >= 3 { 68 | Some(Color32::from_rgba_premultiplied( 69 | *subs.first().unwrap(), 70 | *subs.get(1).unwrap(), 71 | *subs.get(2).unwrap(), 72 | *subs.get(3).unwrap_or(&255) 73 | )) 74 | } else { 75 | None 76 | } 77 | } 78 | } 79 | 80 | impl ToConfig for Vec { 81 | fn to_config(&self) -> String { 82 | self.join("|||") 83 | } 84 | 85 | fn to_self(value: &str) -> Option where Self: Sized { 86 | let mut result_vec: Vec = vec![]; 87 | for value in value.split("|||") { 88 | result_vec.push(value.to_string()); 89 | }; 90 | 91 | Some(result_vec) 92 | } 93 | } 94 | 95 | impl ToConfig for bool { 96 | fn to_config(&self) -> String { 97 | if *self { 98 | "true".to_string() 99 | } else { 100 | "false".to_string() 101 | } 102 | } 103 | 104 | fn to_self(value: &str) -> Option where Self: Sized { 105 | match value { 106 | "true" => Some(true), 107 | "false" => Some(false), 108 | _ => None 109 | } 110 | } 111 | } 112 | 113 | impl ToConfig for Vec<&str> { 114 | fn to_config(&self) -> String { 115 | self.join("|||") 116 | } 117 | 118 | fn to_self(_value: &str) -> Option where Self: Sized { 119 | None 120 | } 121 | } 122 | 123 | impl ConfigOption { 124 | 125 | pub fn get(&self) -> Option { 126 | T::to_self(&self.value) 127 | } 128 | 129 | pub fn raw(&self) -> &String { 130 | &self.value 131 | } 132 | 133 | pub fn display_options(&self) -> &String { 134 | &self.display_options 135 | } 136 | } -------------------------------------------------------------------------------- /core/src/data_model/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod app_config; 2 | mod config_option; 3 | mod config_base; 4 | 5 | pub use app_config::AppConfig; 6 | pub use config_option::ConfigOption; 7 | pub use config_base::Config; 8 | pub use config_option::ToConfig; -------------------------------------------------------------------------------- /core/src/data_model/data_model_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | 4 | /*#[test] 5 | fn test_config() { 6 | 7 | }*/ 8 | } -------------------------------------------------------------------------------- /core/src/data_model/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::TicketAdapter; 2 | 3 | /** 4 | The Filter Type describes, what type a Filter is, essentially which option it can have 5 | in the frontend for example 6 | */ 7 | #[derive(Default, PartialEq, Clone)] 8 | pub enum FilterType { 9 | User, 10 | #[default] Builtin, 11 | Bucket(u64), 12 | Tag, 13 | Other 14 | } 15 | 16 | /** 17 | The Filter Identifier is a little Structure, that contains enough data to describe the 18 | Filter uniquely. 19 | */ 20 | #[derive(Default, PartialEq, Clone)] 21 | pub struct FilterIdentifier { 22 | pub adapter: String, 23 | pub name: String 24 | } 25 | 26 | /** 27 | The filter has it's purpose on limiting the output of tickets. 28 | the adapter is the adapter the filter is from (not the adapter it is for). 29 | depending on it's filter_type it will be sorted differently in the user 30 | interface. Usually a user generated filter can be created within the frontend 31 | and has the corresponding menu points in there. if the filter is builtin, it is 32 | a filter that is made, when the adapter is created, there are some other types, 33 | such as a bucket filter, and a tag based filter. the operation is an adapter 34 | specific string, that determines the tickets that get listed. 35 | 36 | The operations structure usually is the following: 37 | ```[[adaptername: operation]]``` 38 | 39 | You can chain multiple of these. 40 | */ 41 | #[derive(Default, PartialEq, Clone)] 42 | pub struct Filter { 43 | pub identifier: FilterIdentifier, 44 | pub operation: String, 45 | pub filter_type: FilterType, 46 | } 47 | 48 | impl Filter { 49 | 50 | pub fn with_adapter(mut self, adapter: &dyn TicketAdapter) -> Self { 51 | self.identifier.adapter = adapter.get_name(); 52 | self 53 | } 54 | 55 | pub fn with_type(mut self, filter_type: FilterType) -> Self { 56 | self.filter_type = filter_type; 57 | self 58 | } 59 | 60 | pub fn with_details(mut self, name: String, operation: String) -> Self { 61 | self.identifier.name = name; 62 | self.operation = operation; 63 | self 64 | } 65 | } 66 | 67 | impl Filter { 68 | 69 | pub fn filter_expression(adapter: String, inner_expression: &str) -> String { 70 | ["[[", adapter.as_str(), ": ", inner_expression, "]]"].join("") 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /core/src/data_model/local_database.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{ 3 | Arc, 4 | Mutex 5 | }, 6 | time::{ 7 | SystemTime, 8 | UNIX_EPOCH 9 | } 10 | }; 11 | 12 | use rusqlite::{Connection, Error}; 13 | 14 | 15 | pub struct LocalDatabase { 16 | pub connection: Arc> 17 | } 18 | 19 | impl LocalDatabase { 20 | 21 | fn _now_timestamp() -> u64 { 22 | SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() 23 | } 24 | 25 | pub fn create_table(&mut self, tablename: &String, attributes: Vec) -> bool { 26 | let connection = self.connection.lock().unwrap(); 27 | let mut stmt_table_exists = connection.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=(?)").unwrap(); 28 | let mut rows = stmt_table_exists.query([tablename]).unwrap(); 29 | 30 | let mut tables: Vec = Vec::new(); 31 | while let Some(row) = rows.next().unwrap() { 32 | tables.push(row.get(0).unwrap()); 33 | } 34 | 35 | if tables.is_empty() { 36 | 37 | let expression = ["CREATE TABLE ", tablename.as_str(), "(", attributes.join(", ").as_str(), ");"].join(""); 38 | 39 | let mut stmt_table_create = connection.prepare(expression.as_str()).unwrap(); 40 | stmt_table_create.execute([]).unwrap(); 41 | true 42 | } else { 43 | false 44 | } 45 | } 46 | 47 | pub fn open(path: String) -> Result { 48 | match Connection::open(path.as_str()) { 49 | Ok(conn) => Ok(LocalDatabase{connection: Arc::new(Mutex::new(conn))}), 50 | Err(err) => Err(err), 51 | } 52 | 53 | 54 | } 55 | } -------------------------------------------------------------------------------- /core/src/data_model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tag; 2 | mod ticket; 3 | mod bucket; 4 | mod filter; 5 | mod state; 6 | mod config; 7 | mod local_database; 8 | mod bucket_panel_location; 9 | mod data_model_tests; 10 | 11 | pub use tag::Tag as Tag; 12 | pub use ticket::Ticket as Ticket; 13 | pub use bucket::Bucket as Bucket; 14 | pub use bucket::BucketIdentifier as BucketIdentifier; 15 | pub use filter::Filter as Filter; 16 | pub use filter::FilterType as FilterType; 17 | pub use filter::FilterIdentifier as FilterIdentifier; 18 | pub use state::State as State; 19 | pub use state::StateIdentifier as StateIdentifier; 20 | pub use config::AppConfig as AppConfig; 21 | pub use config::Config as Config; 22 | pub use config::ConfigOption as ConfigOption; 23 | pub use config::ToConfig as ToConfig; 24 | pub use local_database::LocalDatabase as LocalDatabase; 25 | pub use bucket_panel_location::BucketPanelLocation as BucketPanelLocation; 26 | pub use bucket_panel_location::BucketPanelLocationType as BucketPanelLocationType; -------------------------------------------------------------------------------- /core/src/data_model/state.rs: -------------------------------------------------------------------------------- 1 | use crate::TicketAdapter; 2 | 3 | #[derive(Default, PartialEq, Clone, Eq, Hash)] 4 | pub struct StateIdentifier { 5 | pub adapter: String, 6 | pub name: String, 7 | } 8 | 9 | impl StateIdentifier { 10 | pub fn new(adapter: &String, name: &String) -> Self { 11 | StateIdentifier { adapter: adapter.clone(), name: name.clone() } 12 | } 13 | } 14 | 15 | #[derive(Default, PartialEq, Clone)] 16 | pub struct State { 17 | pub identifier: StateIdentifier, 18 | pub description: String, 19 | pub sorting_order: i64 20 | } 21 | 22 | impl State { 23 | 24 | pub fn with_name(mut self, name: String) -> Self { 25 | self.identifier.name = name; 26 | self 27 | } 28 | 29 | pub fn with_order(mut self, sorting_order: i64) -> Self { 30 | self.sorting_order = sorting_order; 31 | self 32 | } 33 | 34 | pub fn with_description(mut self, description: String) -> Self { 35 | self.description = description; 36 | self 37 | } 38 | 39 | pub fn with_adapter(mut self, adapter: &dyn TicketAdapter ) -> Self { 40 | self.identifier.adapter = adapter.get_name(); 41 | self 42 | } 43 | } -------------------------------------------------------------------------------- /core/src/data_model/tag.rs: -------------------------------------------------------------------------------- 1 | use rand::prelude::*; 2 | use std::{fmt::Write, ops::Range}; 3 | use crate::TicketAdapter; 4 | 5 | #[derive(Eq, PartialOrd, Ord, Debug, PartialEq, Clone)] 6 | pub struct Tag { 7 | pub adapter: String, 8 | pub name: String, 9 | pub color: String, 10 | pub color_text: String 11 | } 12 | 13 | impl Default for Tag { 14 | fn default() -> Self { 15 | Tag { 16 | adapter: String::default(), 17 | name: String::default(), 18 | color: String::from("#ffffffff"), 19 | color_text: String::from("#000000ff") 20 | } 21 | } 22 | } 23 | 24 | impl Tag { 25 | 26 | pub fn generate_random_color( 27 | hue_range: Range, 28 | sat_range: Range, 29 | val_range: Range) -> (f32, f32, f32) { 30 | let mut rng = rand::thread_rng(); 31 | let (hue, sat, val); 32 | 33 | hue = rng.gen_range(hue_range); 34 | sat = rng.gen_range(sat_range); 35 | val = rng.gen_range(val_range); 36 | 37 | (hue, sat, val) 38 | } 39 | 40 | pub fn with_random_colors(mut self) -> Self { 41 | let mut rng = rand::thread_rng(); 42 | let (hue, sat, val); 43 | let (hue_text, sat_text, val_text); 44 | let choice: u8 = rng.gen_range(1..=6); 45 | 46 | match choice { 47 | 1 => { 48 | //Colorful background with dark Text 49 | hue = rng.gen_range(0.0 .. 1.0); 50 | sat = rng.gen_range(0.5 ..= 1.0); 51 | val = 0.9; 52 | hue_text = hue; 53 | sat_text = sat; 54 | val_text = 0.15; 55 | }, 56 | 2 => { 57 | //light colored Background with Colored Text 58 | hue = rng.gen_range(0.0 .. 1.0); 59 | sat = rng.gen_range(0.0 ..= 0.25); 60 | val = 0.9; 61 | hue_text = hue; 62 | sat_text = 1.0; 63 | val_text = 0.8; 64 | }, 65 | 3 => { 66 | //dark colored Background with light text 67 | hue = rng.gen_range(0.0 .. 1.0); 68 | sat = rng.gen_range(0.5 ..= 1.0); 69 | val = 0.15; 70 | hue_text = hue; 71 | sat_text = sat; 72 | val_text = 0.9; 73 | }, 74 | 4 => { 75 | //gray background with black or white text 76 | hue = 0.0; 77 | sat = 0.0; 78 | val = rng.gen_range(0.2 ..= 0.8); 79 | hue_text = hue; 80 | sat_text = sat; 81 | val_text = if val > 0.5 { val - 0.5 } else { val + 0.5 }; 82 | }, 83 | 5 => { 84 | //colorless dark background with colored text 85 | hue = 0.0; 86 | sat = 0.0; 87 | val = rng.gen_range(0.2 ..= 0.4); 88 | 89 | hue_text = rng.gen_range(0.0 .. 1.0); 90 | sat_text = rng.gen_range(0.5 ..= 1.0); 91 | val_text = rng.gen_range(0.6 ..= 1.0); 92 | }, 93 | _ => { 94 | //colorless light background with colored text 95 | hue = 0.0; 96 | sat = 0.0; 97 | val = rng.gen_range(0.6 ..= 0.8); 98 | 99 | hue_text = rng.gen_range(0.0 .. 1.0); 100 | sat_text = rng.gen_range(0.5 ..= 1.0); 101 | val_text = rng.gen_range(0.0 ..= 0.4); 102 | } 103 | } 104 | 105 | let (r, g, b) = Tag::hsv_to_rgb(hue, sat, val); 106 | let (rt, gt, bt) = Tag::hsv_to_rgb(hue_text, sat_text, val_text); 107 | 108 | let mut color_string = "#".to_string(); 109 | write!(&mut color_string, "{:02x}{:02x}{:02x}{:02x}", r, g, b, 255).expect("Failed to create Color string in Tag"); 110 | self.color = color_string; 111 | 112 | let mut color_text_string = "#".to_string(); 113 | write!(&mut color_text_string, "{:02x}{:02x}{:02x}{:02x}", rt, gt, bt, 255).expect("Failed to create Text Color string in Tag"); 114 | self.color_text = color_text_string; 115 | self 116 | } 117 | 118 | pub fn with_hex_colors(mut self, background: &str, text: &str) -> Self { 119 | self.color = String::from(background); 120 | self.color_text = String::from(text); 121 | self 122 | } 123 | 124 | pub fn with_hex_color(mut self, text: &str) -> Self { 125 | 126 | let without_prefix = text.trim_start_matches("#"); 127 | let r = u8::from_str_radix(without_prefix.get(..2).unwrap(), 16).unwrap(); 128 | let g = u8::from_str_radix(without_prefix.get(2..4).unwrap(), 16).unwrap(); 129 | let b = u8::from_str_radix(without_prefix.get(4..6).unwrap(), 16).unwrap(); 130 | 131 | self.color_text = format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, 255); 132 | self.color = format!("#{:02x}{:02x}{:02x}{:02x}", r/3, g/3, b/3, 128); 133 | self 134 | } 135 | 136 | pub fn with_name(mut self, name: String) -> Self { 137 | self.name = name; 138 | self 139 | } 140 | 141 | pub fn with_adapter(mut self, adapter: &dyn TicketAdapter) -> Self { 142 | self.adapter = adapter.get_name(); 143 | self 144 | } 145 | 146 | //hue 0-1, 147 | //saturation 0-1, 148 | //value 0-1 149 | pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) { 150 | 151 | let v2 = (255.0 * v) as u8; 152 | 153 | if s == 0.0 { 154 | (v2, v2, v2) 155 | } else { 156 | let i = (h * 6.0) as u8; 157 | let f = ((h*6.0) as u8) as f32 - (i as f32); 158 | let p = (255.0 * (v * (1.0 - s))) as u8; 159 | let q = (255.0 * (v * (1.0 - s * f))) as u8; 160 | let t = (255.0 * (v * (1.0 - s * (1.0 - f)))) as u8; 161 | 162 | match i%6 { 163 | 0 => (v2, t, p), 164 | 1 => (q, v2, p), 165 | 2 => (p, v2, t), 166 | 3 => (p, q, v2), 167 | 4 => (t, p, v2), 168 | _ => (v2, p, q) 169 | } 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /core/src/data_model/ticket.rs: -------------------------------------------------------------------------------- 1 | use std::time::{ 2 | SystemTime, 3 | UNIX_EPOCH 4 | }; 5 | 6 | use crate::TicketAdapter; 7 | 8 | use super::{ 9 | Bucket, 10 | State, 11 | Tag 12 | }; 13 | 14 | #[derive(Eq, PartialOrd, Ord, Debug, PartialEq, Clone)] 15 | pub struct Ticket { 16 | pub adapter: String, 17 | pub id: i64, 18 | pub bucket_id: u64, 19 | pub title: String, 20 | pub assigned_to: String, 21 | pub state_name: String, 22 | pub description: String, 23 | pub tags: Vec, 24 | pub created_at: i64, 25 | pub due_at: i64, 26 | pub additional_id: String 27 | } 28 | 29 | impl Default for Ticket { 30 | fn default() -> Self { 31 | Ticket { 32 | adapter: "".into(), 33 | id: 0, 34 | bucket_id: 0, 35 | title: "".into(), 36 | assigned_to: "".into(), 37 | state_name: "".into(), 38 | description: "".into(), 39 | created_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, 40 | due_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, 41 | tags: vec![], 42 | additional_id: "".into() 43 | } 44 | } 45 | } 46 | 47 | impl Ticket { 48 | 49 | pub fn with_adapter(mut self, adapter: &dyn TicketAdapter) -> Self { 50 | self.adapter = adapter.get_name(); 51 | self 52 | } 53 | 54 | pub fn with_details(mut self, id: i64, title: String, description: String) -> Self { 55 | self.id = id; 56 | self.title = title; 57 | self.description = description; 58 | self 59 | } 60 | 61 | pub fn with_bucket(mut self, bucket: &Bucket) -> Self { 62 | self.bucket_id = bucket.identifier.id; 63 | self 64 | } 65 | 66 | pub fn with_state(mut self, state: &State) -> Self { 67 | self.state_name = state.identifier.name.clone(); 68 | self 69 | } 70 | 71 | pub fn with_tags(mut self, tags: Vec<&Tag>) -> Self { 72 | for tag in tags { 73 | self.tags.push(tag.name.clone()); 74 | } 75 | 76 | self 77 | } 78 | 79 | pub fn with_assignee(mut self, assignee: String) -> Self { 80 | self.assign_to(assignee); 81 | self 82 | } 83 | 84 | pub fn add_tag(&mut self, tag: &Tag) { 85 | if !self.tags.contains(&tag.name) { 86 | self.tags.push(tag.name.clone()) 87 | } 88 | } 89 | 90 | pub fn assign_to(&mut self, assignee: String) { 91 | self.assigned_to = assignee; 92 | } 93 | } -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod data_model; 2 | mod adapter_base; 3 | mod ticket_provider; 4 | 5 | pub use data_model::Tag as Tag; 6 | pub use data_model::Ticket as Ticket; 7 | pub use data_model::Bucket as Bucket; 8 | pub use data_model::BucketIdentifier as BucketIdentifier; 9 | pub use data_model::Filter as Filter; 10 | pub use data_model::FilterIdentifier as FilterIdentifier; 11 | pub use data_model::FilterType as FilterType; 12 | pub use data_model::State as State; 13 | pub use data_model::StateIdentifier as StateIdentifier; 14 | pub use data_model::AppConfig as AppConfig; 15 | pub use data_model::Config as Config; 16 | pub use data_model::ConfigOption as ConfigOption; 17 | pub use data_model::ToConfig as ToConfig; 18 | pub use data_model::LocalDatabase as LocalDatabase; 19 | pub use data_model::BucketPanelLocation as BucketPanelLocation; 20 | pub use data_model::BucketPanelLocationType as BucketPanelLocationType; 21 | 22 | pub use adapter_base::AdapterError; 23 | pub use adapter_base::AdapterErrorType; 24 | pub use adapter_base::TicketAdapter; 25 | 26 | pub use ticket_provider::TicketProvider; 27 | pub use ticket_provider::AdapterConstructor; 28 | pub use ticket_provider::AdapterType; -------------------------------------------------------------------------------- /main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // Uncomment for debugging build 2 | 3 | use std::{ 4 | sync::{ 5 | Arc, 6 | Mutex 7 | }, 8 | }; 9 | 10 | use tickets_rs_adapters::{ 11 | LocalTicketAdapter, 12 | GithubTicketAdapter 13 | }; 14 | 15 | use tickets_rs_core::{ 16 | AppConfig, 17 | LocalDatabase, 18 | TicketProvider, 19 | AdapterType 20 | }; 21 | 22 | use tickets_rs_ui::{ 23 | UserInterface, 24 | UIController, 25 | UITheme 26 | }; 27 | 28 | 29 | #[tokio::main] 30 | async fn main() { 31 | let database = { 32 | let database = match LocalDatabase::open("./app_config.db3".to_string()) { 33 | Ok(success) => success, 34 | Err(_) => { 35 | println!("Failed to read Local SQLite Database, exiting!"); return; 36 | } 37 | }; 38 | Arc::new(Mutex::new(database)) 39 | }; 40 | 41 | let update_trigger = Arc::new(Mutex::new(false)); 42 | let configuration = Arc::new(Mutex::new(AppConfig::new(database))); 43 | let ticket_provider = Arc::new(Mutex::new( { 44 | TicketProvider::new(configuration.clone(), vec![ 45 | 46 | AdapterType::new::(), 47 | AdapterType::new::(), 48 | 49 | ], update_trigger.clone()) 50 | })); 51 | 52 | 53 | let ui_controller = UIController::new(configuration.clone(), ticket_provider, update_trigger); 54 | let ui_theme = UITheme::from(configuration); 55 | UserInterface::launch(ui_controller, ui_theme); 56 | 57 | } -------------------------------------------------------------------------------- /ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | rust-version = "1.70" 3 | name = "tickets-rs-ui" 4 | version = "0.1.0" 5 | authors = ["Robert Lang"] 6 | edition = "2021" 7 | description = "This crate contains all UI related things for tickets-rs, made with iced" 8 | 9 | [dependencies] 10 | tickets-rs-core = { version = "0.1", path = "../core" } 11 | 12 | egui = { version = "0.23.0", features = ["color-hex"] } 13 | egui_extras = { version = "0.23.0", features = ["datepicker"]} 14 | # egui-datepicker = { version = "0.3.0" } 15 | egui_commonmark = { version = "0.8.0", features = ["svg", "fetch"] } 16 | eframe = "0.23.0" 17 | chrono = "0.4.22" 18 | timer = "0.2.0" 19 | png = "0.17.6" 20 | arboard = "3.2.1" -------------------------------------------------------------------------------- /ui/src/helper.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{Response, RichText}; 2 | 3 | use crate::UITheme; 4 | 5 | pub struct UIHelper {} 6 | 7 | impl UIHelper { 8 | 9 | pub fn extend_tooltip(response: Response, ui_theme: &UITheme, text: &str) -> Response { 10 | response.on_hover_text_at_pointer(UIHelper::sized_text(ui_theme, 1.0, text)) 11 | } 12 | 13 | pub fn sized_text(ui_theme: &UITheme, factor: f32, text: &str) -> RichText { 14 | RichText::new(text).size(ui_theme.font_size as f32 * factor) 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /ui/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod ui_controller; 2 | mod user_interface; 3 | mod ui_theme; 4 | mod overlays; 5 | mod ui_cache; 6 | mod helper; 7 | 8 | pub use overlays::Overlay; 9 | pub use ui_controller::UIController; 10 | pub use ui_theme::UITheme; 11 | pub use user_interface::UserInterface; 12 | pub use helper::UIHelper; 13 | 14 | pub use ui_cache::*; 15 | 16 | pub const APP_VERSION: &str = "0.2023.04.1"; -------------------------------------------------------------------------------- /ui/src/overlays/mod.rs: -------------------------------------------------------------------------------- 1 | mod helper; 2 | mod overlay_tag; 3 | mod overlay_ticket; 4 | mod overlay_about; 5 | mod overlay_wizard; 6 | mod overlay_state; 7 | mod overlay_bucket; 8 | mod overlay_preferences; 9 | mod overlay_adapter; 10 | mod overlay_filter; 11 | 12 | use std::collections::{ 13 | HashMap, 14 | hash_map::RandomState 15 | }; 16 | use eframe::egui::{ 17 | Ui, 18 | Color32, TextureHandle, ColorImage, Image, Vec2, RichText, Button, 19 | }; 20 | 21 | use chrono::{Date, Utc, TimeZone, Duration, DateTime, Datelike, Timelike, offset}; 22 | use tickets_rs_core::{State, Filter}; 23 | use tickets_rs_core::{Ticket, Bucket, Tag}; 24 | 25 | use helper::OverlayHelper as OverlayHelper; 26 | use crate::{UITheme, UserInterface, UIController, ui_controller, UICache}; 27 | 28 | pub use self::overlay_tag::NewTagData; 29 | pub use self::overlay_ticket::EditTicketData; 30 | pub use self::overlay_ticket::NewTicketData; 31 | pub use self::overlay_ticket::UpdateTicketData; 32 | pub use self::overlay_ticket::UpdateTicketDataAssign; 33 | pub use self::overlay_ticket::UpdateTicketDataBucket; 34 | pub use self::overlay_ticket::UpdateTicketDataAdapter; 35 | pub use self::overlay_wizard::WizardData; 36 | pub use self::overlay_state::NewStateData; 37 | pub use self::overlay_bucket::NewBucketData; 38 | pub use self::overlay_bucket::DeleteBucketData; 39 | pub use self::overlay_preferences::PreferenceData; 40 | pub use self::overlay_adapter::DeleteAdapterData; 41 | pub use self::overlay_filter::NewFilterData; 42 | pub use self::overlay_filter::EditFilterData; 43 | pub use self::overlay_filter::DeleteFilterData; 44 | 45 | #[derive(PartialEq, Clone)] 46 | pub enum Overlay { 47 | None, 48 | 49 | Wizard(WizardData), 50 | About, 51 | Preferences(PreferenceData), 52 | 53 | NewTicket(NewTicketData), 54 | EditTicket(EditTicketData), 55 | UpdateTicketState(UpdateTicketData), 56 | UpdateTicketDetails(UpdateTicketData), 57 | UpdateTicketBucket(UpdateTicketDataBucket), 58 | UpdateTicketAssign(UpdateTicketDataAssign), 59 | UpdateTicketAdapter(UpdateTicketDataAdapter), 60 | DeleteTicket(UpdateTicketData), 61 | 62 | NewBucket(NewBucketData), 63 | DeleteBucket(DeleteBucketData), 64 | 65 | NewState(NewStateData), 66 | 67 | DeleteAdapter(DeleteAdapterData), 68 | 69 | NewTag(NewTagData), 70 | 71 | NewFilter(NewFilterData), 72 | EditFilter(EditFilterData), 73 | DeleteFilter(DeleteFilterData) 74 | 75 | } 76 | 77 | #[derive(PartialEq, Clone)] 78 | pub enum DialogOptions { 79 | Nothing, 80 | Close, 81 | Confirm 82 | } 83 | 84 | #[derive(PartialEq)] 85 | pub enum OverlayAction { 86 | Nothing, 87 | CloseOverlay, 88 | 89 | NewTicket(Ticket), 90 | UpdateTicket(Ticket), 91 | UpdateTicketAdapter(Ticket, String), //Old Adapter Name 92 | DeleteTicket(Ticket), 93 | 94 | NewTag(Tag), 95 | UpdateTag(Tag), 96 | DeleteTag(Tag), 97 | 98 | NewState(State), 99 | UpdateState(State), 100 | 101 | NewBucket(Bucket), 102 | UpdateBucket(Bucket), 103 | DeleteBucket(Bucket), 104 | 105 | WizardDone(WizardData), 106 | PreferencesApply(PreferenceData), 107 | 108 | DeleteAdapter(String), 109 | 110 | NewFilter(Filter), 111 | EditFilter(Filter), 112 | DeleteFilter(Filter) 113 | } 114 | 115 | impl Overlay { 116 | 117 | pub fn put_errors(overlay: &mut Overlay, errors: &mut Vec<(String, String)>){ 118 | let overlay_errors = match overlay { 119 | 120 | Overlay::NewTicket(ticket_data) => &mut ticket_data.errors, 121 | Overlay::EditTicket(ticket_data) => &mut ticket_data.errors, 122 | Overlay::UpdateTicketState(ticket_data) => &mut ticket_data.errors, 123 | Overlay::UpdateTicketDetails(ticket_data) => &mut ticket_data.errors, 124 | Overlay::UpdateTicketBucket(ticket_data) => &mut ticket_data.errors, 125 | Overlay::UpdateTicketAssign(ticket_data) => &mut ticket_data.errors, 126 | Overlay::UpdateTicketAdapter(ticket_data) => &mut ticket_data.errors, 127 | Overlay::DeleteTicket(ticket_data) => &mut ticket_data.errors, 128 | Overlay::NewTag(tag_data) => &mut tag_data.errors, 129 | Overlay::NewState(state_data) => &mut state_data.errors, 130 | Overlay::NewBucket(bucket_data) => &mut bucket_data.errors, 131 | Overlay::NewFilter(filter_data) => &mut filter_data.errors, 132 | Overlay::EditFilter(filter_data) => &mut filter_data.errors, 133 | Overlay::DeleteFilter(filter_data) => &mut filter_data.errors, 134 | _ => return 135 | }; 136 | 137 | overlay_errors.clear(); 138 | overlay_errors.append(errors) 139 | 140 | } 141 | 142 | pub fn update( 143 | overlay: &mut Overlay, 144 | ui: &mut Ui, 145 | ui_theme: &mut UITheme, 146 | ui_controller: &mut UIController, 147 | cache: &mut UICache, 148 | icon_textures: &mut HashMap, RandomState>, 149 | icons: &mut HashMap, RandomState>, 150 | 151 | ) -> OverlayAction { 152 | match overlay { 153 | 154 | Overlay::None => OverlayAction::Nothing, 155 | Overlay::Wizard(wizard_data) => Overlay::update_wizard(ui, ui_theme, ui_controller, wizard_data), 156 | Overlay::Preferences(preference_data) => Overlay::update_preferences(ui, ui_theme, ui_controller, preference_data), 157 | Overlay::NewBucket(bucket_data) => Overlay::update_new_bucket(ui, ui_theme, bucket_data), 158 | Overlay::NewTicket(ticket_data) => Overlay::update_new_ticket(ui, ui_theme, ticket_data, cache), 159 | Overlay::NewState(state_data) => Overlay::update_new_state(ui, ui_theme, state_data), 160 | Overlay::UpdateTicketState(ticket_data) => Overlay::update_ticket_state(ui, ui_theme, cache, ticket_data), 161 | Overlay::UpdateTicketDetails(ticket_data) => Overlay::update_ticket_details(ui, ui_theme, ticket_data), 162 | Overlay::UpdateTicketBucket(ticket_data) => Overlay::update_ticket_bucket(ui, ui_theme, ticket_data), 163 | Overlay::UpdateTicketAssign(ticket_data) => Overlay::update_ticket_assign(ui, ui_theme, ticket_data), 164 | Overlay::UpdateTicketAdapter(ticket_data) => Overlay::update_ticket_adapter(ui, ui_theme, ticket_data), 165 | Overlay::DeleteTicket(ticket_data) => Overlay::update_delete_ticket(ui, ui_theme, ticket_data), 166 | Overlay::About => Overlay::update_about(ui, ui_theme, icon_textures, icons), 167 | Overlay::NewTag(tag_data) => Overlay::update_new_tag(ui, ui_theme, tag_data), 168 | Overlay::EditTicket(ticket_data) => Overlay::update_edit_ticket(ui, ui_theme, ticket_data, cache), 169 | Overlay::DeleteAdapter(adapter_data) => Overlay::update_delete_adapter(ui, ui_theme, adapter_data), 170 | Overlay::NewFilter(filter_data) => Overlay::update_new_filter(ui, ui_theme, filter_data), 171 | Overlay::EditFilter(filter_data) => Overlay::update_edit_filter(ui, ui_theme, filter_data), 172 | Overlay::DeleteFilter(filter_data) => Overlay::update_delete_filter(ui, ui_theme, filter_data), 173 | Overlay::DeleteBucket(bucket_data) => Overlay::update_delete_bucket(ui, ui_theme, bucket_data), 174 | 175 | } 176 | } 177 | } 178 | 179 | impl OverlayAction { 180 | pub fn execute(self, ui_controller: &mut UIController, cache: &mut UICache) { 181 | match self { 182 | OverlayAction::UpdateTicket(ticket) => OverlayAction::action_ticket(ui_controller, ticket), 183 | OverlayAction::NewTicket(ticket) => OverlayAction::action_ticket(ui_controller, ticket), 184 | OverlayAction::CloseOverlay => ui_controller.close_overlay(), 185 | OverlayAction::Nothing => (), 186 | OverlayAction::NewState(state) => OverlayAction::action_state(ui_controller, cache, state), 187 | OverlayAction::NewTag(tag) => OverlayAction::action_tag(ui_controller, cache, tag), 188 | OverlayAction::PreferencesApply(preference_data) => OverlayAction::action_preferences(ui_controller, cache, preference_data), 189 | OverlayAction::WizardDone(wizard_data) => OverlayAction::action_wizard(ui_controller, cache, wizard_data), 190 | OverlayAction::UpdateTag(tag) => OverlayAction::action_tag(ui_controller, cache, tag), 191 | OverlayAction::UpdateState(state) => OverlayAction::action_state(ui_controller, cache, state), 192 | OverlayAction::NewBucket(bucket) => OverlayAction::action_bucket(ui_controller, cache, bucket), 193 | OverlayAction::UpdateBucket(bucket) => OverlayAction::action_bucket(ui_controller, cache, bucket), 194 | OverlayAction::DeleteTicket(ticket) => OverlayAction::action_ticket_delete(ui_controller, ticket), 195 | OverlayAction::DeleteTag(tag) => OverlayAction::action_tag_delete(ui_controller, cache, tag), 196 | OverlayAction::DeleteAdapter(adapter_name) => OverlayAction::action_adapter_delete(ui_controller, adapter_name), 197 | OverlayAction::UpdateTicketAdapter(ticket, old_adapter_name) => OverlayAction::action_ticket_adapter(ui_controller, ticket, old_adapter_name), 198 | OverlayAction::NewFilter(filter) => OverlayAction::action_filter(ui_controller, cache, filter), 199 | OverlayAction::EditFilter(filter) => OverlayAction::action_filter(ui_controller, cache, filter), 200 | OverlayAction::DeleteFilter(filter) => OverlayAction::action_filter_delete(ui_controller, cache, filter), 201 | OverlayAction::DeleteBucket(bucket) => OverlayAction::action_bucket_delete(ui_controller, bucket), 202 | }; 203 | } 204 | } -------------------------------------------------------------------------------- /ui/src/overlays/overlay_about.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{ 2 | HashMap, 3 | hash_map::RandomState 4 | }; 5 | 6 | use eframe::egui::{ 7 | Ui, 8 | ColorImage, 9 | TextureHandle, 10 | RichText 11 | }; 12 | 13 | use crate::{ 14 | UITheme, 15 | Overlay, APP_VERSION 16 | }; 17 | 18 | use super::{ 19 | OverlayHelper, 20 | OverlayAction, 21 | DialogOptions 22 | }; 23 | 24 | 25 | impl Overlay { 26 | 27 | pub(crate) fn update_about( 28 | ui: &mut Ui, 29 | ui_theme: &UITheme, 30 | icon_textures: &mut HashMap, RandomState>, 31 | icons: &mut HashMap, RandomState> 32 | ) -> OverlayAction { 33 | OverlayHelper::helper_update_header(ui, ui_theme, "About"); 34 | OverlayHelper::helper_update_spacer(ui, ui_theme); 35 | OverlayHelper::helper_update_icon(ui, icon_textures, icons, &"icon_app".to_string(), 120.0); 36 | OverlayHelper::helper_update_spacer(ui, ui_theme); 37 | ui.label(RichText::new("tickets.rs - A Ticket Management App").strong()); 38 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 39 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Details", true, |ui| { 40 | OverlayHelper::helper_update_card(ui, ui_theme, "Devs".to_string(), |ui| { 41 | ui.label("Robert Lang"); 42 | }); 43 | 44 | OverlayHelper::helper_update_card(ui, ui_theme, "Version".to_string(), |ui| { 45 | ui.label(format!("tickets.rs v{}", APP_VERSION)); 46 | }); 47 | 48 | OverlayHelper::helper_update_card(ui, ui_theme, "History".to_string(), |ui| { 49 | ui.label("Development of prototype has been started in September 2021."); 50 | ui.label("Prototype finished in December 2021, written in Python 3.6"); 51 | ui.label("Testing of the prototype has been continued until October 2022"); 52 | ui.label("The development of the Release Version in Rust has been started in October 2022"); 53 | }); 54 | 55 | }); 56 | 57 | OverlayHelper::helper_update_spacer(ui, ui_theme); 58 | ui.label(RichText::new("The software works as-is, and doesn't need a license.").strong()); 59 | OverlayHelper::helper_update_spacer(ui, ui_theme); 60 | 61 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, None) { 62 | DialogOptions::Nothing => OverlayAction::Nothing, 63 | DialogOptions::Close => OverlayAction::CloseOverlay, 64 | DialogOptions::Confirm => OverlayAction::CloseOverlay, 65 | } 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /ui/src/overlays/overlay_adapter.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Ui; 2 | 3 | use crate::{UIController, UITheme, UICache}; 4 | 5 | use super::{OverlayAction, Overlay, helper::OverlayHelper, DialogOptions}; 6 | 7 | #[derive(Default, PartialEq, Clone)] 8 | pub struct DeleteAdapterData { 9 | pub adapter_name: String 10 | } 11 | 12 | 13 | impl Overlay { 14 | pub(crate) fn update_delete_adapter( 15 | ui: &mut Ui, 16 | ui_theme: &mut UITheme, 17 | adapter_data: &mut DeleteAdapterData 18 | ) -> OverlayAction { 19 | OverlayHelper::helper_update_header(ui, ui_theme, "Delete Adapter"); 20 | 21 | OverlayHelper::helper_update_warning(ui, ui_theme, 22 | format!("Are you absolutely sure, that you want to remove the adapter 23 | \"{}\" from the Program?\nThis will only remove the reference, additional Data might need to be removed by Hand.", 24 | adapter_data.adapter_name).as_str()); 25 | 26 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 27 | 28 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Delete".to_string())) { 29 | DialogOptions::Nothing => OverlayAction::Nothing, 30 | DialogOptions::Close => OverlayAction::CloseOverlay, 31 | DialogOptions::Confirm => OverlayAction::DeleteAdapter(adapter_data.adapter_name.clone()), 32 | } 33 | } 34 | } 35 | 36 | impl OverlayAction { 37 | pub(crate) fn action_adapter_delete(ui_controller: &mut UIController, adapter_name: String) { 38 | ui_controller.close_overlay(); 39 | ui_controller.using_ticket_provider(|_, provider| { 40 | provider.drop_adapter(adapter_name.clone(), true); 41 | }); 42 | ui_controller.trigger_bucket_panel_update(); 43 | } 44 | } -------------------------------------------------------------------------------- /ui/src/overlays/overlay_bucket.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Ui; 2 | use tickets_rs_core::Bucket; 3 | 4 | use crate::{Overlay, UITheme, UIController, UICache}; 5 | 6 | use super::{OverlayAction, helper::OverlayHelper, DialogOptions}; 7 | 8 | 9 | 10 | #[derive(Default, PartialEq, Clone)] 11 | pub struct NewBucketData { 12 | pub bucket: Bucket, 13 | pub adapters: Vec<(String, String)>, 14 | pub errors: Vec<(String, String)>, 15 | } 16 | 17 | #[derive(Default, PartialEq, Clone)] 18 | pub struct DeleteBucketData { 19 | pub bucket: Bucket, 20 | pub errors: Vec<(String, String)>, 21 | } 22 | 23 | impl Overlay { 24 | 25 | pub(crate) fn update_new_bucket( 26 | ui: &mut Ui, 27 | ui_theme: &mut UITheme, 28 | bucket_data: &mut NewBucketData 29 | ) -> OverlayAction { 30 | 31 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Main Content", true, |ui| { 32 | OverlayHelper::helper_update_adapter(ui, ui_theme, &mut bucket_data.bucket.identifier.adapter, &bucket_data.adapters); 33 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 34 | OverlayHelper::helper_update_text(ui, ui_theme, &mut bucket_data.bucket.name, "Name:"); 35 | }); 36 | 37 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 38 | OverlayHelper::helper_update_errors(ui, ui_theme, &bucket_data.errors); 39 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Create Bucket".to_string())) { 40 | DialogOptions::Nothing => OverlayAction::Nothing, 41 | DialogOptions::Close => OverlayAction::CloseOverlay, 42 | DialogOptions::Confirm => OverlayAction::NewBucket(bucket_data.bucket.clone()), 43 | } 44 | } 45 | 46 | pub(crate) fn update_delete_bucket( 47 | ui: &mut Ui, 48 | ui_theme: &UITheme, 49 | bucket_data: &mut DeleteBucketData 50 | ) -> OverlayAction { 51 | OverlayHelper::helper_update_header(ui, ui_theme, "Delete Bucket"); 52 | 53 | OverlayHelper::helper_update_warning(ui, ui_theme, 54 | format!("Are you absolutely sure, that you want to delete the Bucket\n\"{}\"\nfrom the Adapter\n\"{}\"?", 55 | bucket_data.bucket.name, bucket_data.bucket.identifier.adapter).as_str()); 56 | 57 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 58 | OverlayHelper::helper_update_errors(ui, ui_theme, &bucket_data.errors); 59 | 60 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Delete".to_string())) { 61 | DialogOptions::Nothing => OverlayAction::Nothing, 62 | DialogOptions::Close => OverlayAction::CloseOverlay, 63 | DialogOptions::Confirm => OverlayAction::DeleteBucket(bucket_data.bucket.clone()), 64 | } 65 | } 66 | } 67 | 68 | impl OverlayAction { 69 | 70 | pub(crate) fn action_bucket_delete( 71 | ui_controller: &mut UIController, 72 | bucket: Bucket 73 | ) { 74 | let mut action_successful: bool = false; 75 | ui_controller.using_ticket_provider_mut(|controller, provider| { 76 | 77 | match provider.bucket_drop(&bucket) { 78 | Ok(_) => action_successful = true, 79 | Err(error) => { 80 | 81 | let error_message = error.get_text(); 82 | let mut errors = vec![("other".to_string(), error_message)]; 83 | 84 | Overlay::put_errors(controller.get_current_overlay(), &mut errors); 85 | 86 | }, 87 | }; 88 | 89 | }); 90 | 91 | if action_successful { 92 | ui_controller.close_overlay(); 93 | ui_controller.trigger_bucket_panel_update(); 94 | } 95 | } 96 | 97 | pub(crate) fn action_bucket( 98 | ui_controller: &mut UIController, 99 | cache: &mut UICache, 100 | mut bucket: Bucket 101 | ) { 102 | let mut bucket_write_successful = false; 103 | 104 | ui_controller.using_ticket_provider_mut(|controller, provider| { 105 | match provider.bucket_validate(&bucket) { 106 | Ok(_) => { 107 | match provider.bucket_write(&mut bucket) { 108 | Ok(_) => bucket_write_successful = true, 109 | Err(error) => { 110 | 111 | let error_message = error.get_text(); 112 | let mut errors = vec![("other".to_string(), error_message)]; 113 | 114 | Overlay::put_errors(&mut controller.get_current_overlay(), &mut errors); 115 | 116 | }, 117 | }; 118 | }, 119 | Err(adapter_error) => { 120 | 121 | let mut errors = match adapter_error.error_type { 122 | tickets_rs_core::AdapterErrorType::Validate(errors_vec, _) => errors_vec, 123 | _ => { 124 | let error_message = adapter_error.get_text(); 125 | vec![("other".to_string(), error_message)] 126 | } 127 | }; 128 | 129 | Overlay::put_errors(&mut controller.get_current_overlay(), &mut errors); 130 | } 131 | } 132 | }); 133 | 134 | if bucket_write_successful { 135 | ui_controller.trigger_bucket_panel_update(); 136 | ui_controller.close_overlay() 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /ui/src/overlays/overlay_filter.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Ui; 2 | use tickets_rs_core::Filter; 3 | 4 | use crate::{Overlay, UITheme, UICache, UIController}; 5 | 6 | use super::{OverlayAction, helper::OverlayHelper, DialogOptions}; 7 | 8 | #[derive(Default, PartialEq, Clone)] 9 | pub struct NewFilterData { 10 | pub filter: Filter, 11 | pub adapters: Vec<(String, String)>, 12 | pub errors: Vec<(String, String)>, 13 | } 14 | 15 | #[derive(Default, PartialEq, Clone)] 16 | pub struct EditFilterData { 17 | pub filter: Filter, 18 | pub errors: Vec<(String, String)>, 19 | } 20 | 21 | #[derive(Default, PartialEq, Clone)] 22 | pub struct DeleteFilterData { 23 | pub errors: Vec<(String, String)>, 24 | pub filter: Filter 25 | } 26 | 27 | impl Overlay { 28 | pub(crate) fn update_new_filter( 29 | ui: &mut Ui, 30 | ui_theme: &UITheme, 31 | filter_data: &mut NewFilterData 32 | ) -> OverlayAction { 33 | OverlayHelper::helper_update_header(ui, ui_theme, "New Filter"); 34 | 35 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Location", true, |ui| { 36 | OverlayHelper::helper_update_adapter(ui, ui_theme, &mut filter_data.filter.identifier.adapter, &filter_data.adapters); 37 | }); 38 | 39 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Main Content", true, |ui| { 40 | OverlayHelper::helper_update_text(ui, ui_theme, &mut filter_data.filter.identifier.name, "Name:"); 41 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 42 | OverlayHelper::helper_update_desc(ui, ui_theme, &mut filter_data.filter.operation, false); 43 | }); 44 | 45 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 46 | OverlayHelper::helper_update_errors(ui, ui_theme, &filter_data.errors); 47 | 48 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Create".to_string())) { 49 | DialogOptions::Nothing => OverlayAction::Nothing, 50 | DialogOptions::Close => OverlayAction::CloseOverlay, 51 | DialogOptions::Confirm => OverlayAction::NewFilter(filter_data.filter.clone()), 52 | } 53 | } 54 | 55 | pub(crate) fn update_edit_filter( 56 | ui: &mut Ui, 57 | ui_theme: &UITheme, 58 | filter_data: &mut EditFilterData 59 | ) -> OverlayAction { 60 | 61 | OverlayHelper::helper_update_header(ui, ui_theme, "Edit Filter"); 62 | 63 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Main Content", true, |ui| { 64 | OverlayHelper::helper_update_desc(ui, ui_theme, &mut filter_data.filter.operation, false); 65 | }); 66 | 67 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 68 | OverlayHelper::helper_update_errors(ui, ui_theme, &filter_data.errors); 69 | 70 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Update".to_string())) { 71 | DialogOptions::Nothing => OverlayAction::Nothing, 72 | DialogOptions::Close => OverlayAction::CloseOverlay, 73 | DialogOptions::Confirm => OverlayAction::EditFilter(filter_data.filter.clone()), 74 | } 75 | } 76 | 77 | pub(crate) fn update_delete_filter( 78 | ui: &mut Ui, 79 | ui_theme: &UITheme, 80 | filter_data: &mut DeleteFilterData 81 | ) -> OverlayAction { 82 | OverlayHelper::helper_update_header(ui, ui_theme, "Delete Filter"); 83 | 84 | OverlayHelper::helper_update_warning(ui, ui_theme, 85 | format!("Are you absolutely sure, that you want to delete the Filter\n\"{}\"\nfrom the Adapter\n\"{}\"?", 86 | filter_data.filter.identifier.name, filter_data.filter.identifier.adapter).as_str()); 87 | 88 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 89 | OverlayHelper::helper_update_errors(ui, ui_theme, &filter_data.errors); 90 | 91 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Delete".to_string())) { 92 | DialogOptions::Nothing => OverlayAction::Nothing, 93 | DialogOptions::Close => OverlayAction::CloseOverlay, 94 | DialogOptions::Confirm => OverlayAction::DeleteFilter(filter_data.filter.clone()), 95 | } 96 | } 97 | } 98 | 99 | impl OverlayAction { 100 | pub(crate) fn action_filter( 101 | ui_controller: &mut UIController, 102 | cache: &mut UICache, 103 | filter: Filter 104 | ) { 105 | let mut action_successful: bool = false; 106 | ui_controller.using_ticket_provider_mut(|controller, provider| { 107 | match provider.filter_validate(&filter) { 108 | Ok(_) => { 109 | match provider.filter_write(&filter) { 110 | Ok(_) => action_successful = true, 111 | Err(error) => { 112 | 113 | let error_message = error.get_text(); 114 | let mut errors = vec![("other".to_string(), error_message)]; 115 | 116 | Overlay::put_errors(controller.get_current_overlay(), &mut errors); 117 | 118 | }, 119 | }; 120 | }, 121 | Err(adapter_error) => { 122 | 123 | let mut errors = match adapter_error.error_type { 124 | tickets_rs_core::AdapterErrorType::Validate(errors_vec, _) => errors_vec, 125 | _ => { 126 | let error_message = adapter_error.get_text(); 127 | vec![("other".to_string(), error_message)] 128 | } 129 | }; 130 | 131 | Overlay::put_errors(&mut controller.get_current_overlay(), &mut errors); 132 | } 133 | } 134 | }); 135 | 136 | if action_successful { 137 | ui_controller.close_overlay(); 138 | ui_controller.trigger_bucket_panel_update(); 139 | ui_controller.execute_bucket_panel_selection(); 140 | } 141 | } 142 | 143 | pub(crate) fn action_filter_delete( 144 | ui_controller: &mut UIController, 145 | cache: &mut UICache, 146 | filter: Filter 147 | ) { 148 | let mut action_successful: bool = false; 149 | ui_controller.using_ticket_provider_mut(|controller, provider| { 150 | 151 | match provider.filter_drop(&filter) { 152 | Ok(_) => action_successful = true, 153 | Err(error) => { 154 | 155 | let error_message = error.get_text(); 156 | let mut errors = vec![("other".to_string(), error_message)]; 157 | 158 | Overlay::put_errors(controller.get_current_overlay(), &mut errors); 159 | 160 | }, 161 | }; 162 | 163 | }); 164 | 165 | if action_successful { 166 | ui_controller.close_overlay(); 167 | ui_controller.trigger_bucket_panel_update(); 168 | ui_controller.execute_bucket_panel_selection(); 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /ui/src/overlays/overlay_preferences.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Ui; 2 | 3 | use crate::{Overlay, UITheme, ui_controller, UIController, UICache}; 4 | 5 | use super::{OverlayAction, helper::OverlayHelper, DialogOptions}; 6 | 7 | use tickets_rs_core::Config; 8 | 9 | 10 | #[derive(Default, PartialEq, Clone)] 11 | pub struct PreferenceData { 12 | pub username: String, 13 | pub extension_config: Option, 14 | } 15 | 16 | impl Overlay { 17 | pub(crate) fn update_preferences(ui: &mut Ui, mut ui_theme: &mut UITheme, ui_controller: &mut UIController, preference_data: &mut PreferenceData) -> OverlayAction { 18 | OverlayHelper::helper_update_header(ui, ui_theme, "Preferences"); 19 | 20 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "General Info", false, |ui| { 21 | OverlayHelper::helper_update_text(ui, ui_theme, &mut preference_data.username, "Username:"); 22 | }); 23 | 24 | let ui_theme_copy = ui_theme.clone(); 25 | 26 | OverlayHelper::helper_update_section_collapsing(ui, &ui_theme_copy, "Appearance", false, |ui| { 27 | let old_font_size = ui_theme.font_size; 28 | OverlayHelper::helper_update_theme(ui, &ui_theme_copy, ui_theme, ui_controller); 29 | ui_theme.font_size = old_font_size; 30 | 31 | if OverlayHelper::helper_update_number(ui, &ui_theme_copy, &mut ui_theme.font_size, "Font size:") { 32 | ui_controller.font_changed = true; 33 | }; 34 | }); 35 | 36 | OverlayHelper::helper_update_section_collapsing(ui, &ui_theme_copy, "Additional Appearance Details", false, |ui| { 37 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Primary Background", &mut ui_theme.background_primary); 38 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Secondary Background", &mut ui_theme.background_secondary); 39 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Tertiary Background", &mut ui_theme.background_tertiary); 40 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Error Background", &mut ui_theme.background_error); 41 | OverlayHelper::helper_update_spacer(ui, &ui_theme_copy); 42 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Primary Text", &mut ui_theme.foreground_primary); 43 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Secondary Text", &mut ui_theme.foreground_secondary); 44 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Tertiary Text", &mut ui_theme.foreground_tertiary); 45 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Marker Text", &mut ui_theme.foreground_marker); 46 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Second Marker Text", &mut ui_theme.foreground_marker2); 47 | }); 48 | 49 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Extensions", false, |ui| { 50 | OverlayHelper::helper_update_extensions( 51 | ui, 52 | ui_theme, 53 | ui_controller.ticket_provider.clone(), 54 | &mut preference_data.extension_config, 55 | ui_controller.update_panel_data.clone()); 56 | }); 57 | 58 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, None) { 59 | DialogOptions::Nothing => OverlayAction::Nothing, 60 | _ => OverlayAction::PreferencesApply(preference_data.clone()), 61 | } 62 | } 63 | } 64 | 65 | impl OverlayAction { 66 | pub(crate) fn action_preferences(ui_controller: &mut UIController, cache: &mut UICache, preference_data: PreferenceData) { 67 | ui_controller.close_overlay(); 68 | ui_controller.invalidate_cache(Some(cache)); 69 | ui_controller.trigger_bucket_panel_update(); 70 | match ui_controller.configuration.lock() { 71 | Ok(mut config_lock) => { 72 | config_lock.put("username", preference_data.username, ""); 73 | cache.username_valid = false; 74 | }, 75 | Err(err) => println!("Wasn't able to access app config at the end of the preferences due to {err}") 76 | }; 77 | } 78 | } -------------------------------------------------------------------------------- /ui/src/overlays/overlay_state.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Ui; 2 | use tickets_rs_core::State; 3 | 4 | use crate::{Overlay, UITheme, UIController, UICache}; 5 | 6 | use super::{OverlayAction, helper::OverlayHelper, DialogOptions}; 7 | 8 | 9 | 10 | #[derive(Default, PartialEq, Clone)] 11 | pub struct NewStateData { 12 | pub state: State, 13 | pub adapters: Vec<(String, String)>, 14 | pub errors: Vec<(String, String)>, 15 | } 16 | 17 | impl Overlay { 18 | 19 | pub(crate) fn update_new_state( 20 | ui: &mut Ui, 21 | ui_theme: &mut UITheme, 22 | state_data: &mut NewStateData 23 | ) -> OverlayAction { 24 | 25 | OverlayHelper::helper_update_header(ui, ui_theme, "New State"); 26 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Location & Sorting", true, |ui| { 27 | OverlayHelper::helper_update_adapter(ui, ui_theme, &mut state_data.state.identifier.adapter, &state_data.adapters); 28 | }); 29 | 30 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Main Content", true, |ui| { 31 | OverlayHelper::helper_update_text(ui, ui_theme, &mut state_data.state.identifier.name, "Name:"); 32 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 33 | OverlayHelper::helper_update_text(ui, ui_theme, &mut state_data.state.description, "Description:"); 34 | }); 35 | 36 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Additional", false, |ui| { 37 | OverlayHelper::helper_update_number64(ui, ui_theme, &mut state_data.state.sorting_order, "Sorting Order"); 38 | }); 39 | 40 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 41 | OverlayHelper::helper_update_errors(ui, ui_theme, &state_data.errors); 42 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Create State".to_string())) { 43 | DialogOptions::Nothing => OverlayAction::Nothing, 44 | DialogOptions::Close => OverlayAction::CloseOverlay, 45 | DialogOptions::Confirm => OverlayAction::NewState(state_data.state.clone()), 46 | } 47 | } 48 | } 49 | 50 | impl OverlayAction { 51 | pub(crate) fn action_state( 52 | ui_controller: &mut UIController, 53 | cache: &mut UICache, 54 | state: State 55 | ) { 56 | ui_controller.using_ticket_provider_mut(|controller, provider| { 57 | match provider.state_validate(&state) { 58 | Ok(_) => { 59 | match provider.state_write(&state) { 60 | Ok(_) => { 61 | cache.states_valid = false; 62 | controller.close_overlay() 63 | }, 64 | Err(error) => { 65 | 66 | let error_message = error.get_text(); 67 | let mut errors = vec![("other".to_string(), error_message)]; 68 | 69 | Overlay::put_errors(&mut controller.get_current_overlay(), &mut errors); 70 | 71 | }, 72 | }; 73 | }, 74 | Err(adapter_error) => { 75 | 76 | let mut errors = match adapter_error.error_type { 77 | tickets_rs_core::AdapterErrorType::Validate(errors_vec, _) => errors_vec, 78 | _ => { 79 | let error_message = adapter_error.get_text(); 80 | vec![("other".to_string(), error_message)] 81 | } 82 | }; 83 | 84 | Overlay::put_errors(&mut controller.get_current_overlay(), &mut errors); 85 | } 86 | } 87 | }); 88 | } 89 | } -------------------------------------------------------------------------------- /ui/src/overlays/overlay_tag.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{Ui, Color32}; 2 | use tickets_rs_core::Tag; 3 | 4 | use crate::{ 5 | Overlay, 6 | UITheme, 7 | UIController, UICache 8 | }; 9 | 10 | use super::{ 11 | OverlayHelper, 12 | OverlayAction, 13 | DialogOptions 14 | }; 15 | 16 | 17 | #[derive(Default, PartialEq, Clone)] 18 | pub struct NewTagData { 19 | pub tag: Tag, 20 | pub font_color: Color32, 21 | pub back_color: Color32, 22 | pub adapters: Vec<(String, String)>, 23 | pub errors: Vec<(String, String)>, 24 | } 25 | 26 | impl Overlay { 27 | 28 | pub(crate) fn update_new_tag( 29 | ui: &mut Ui, 30 | ui_theme: &mut UITheme, 31 | tag_data: &mut NewTagData 32 | ) -> OverlayAction { 33 | OverlayHelper::helper_update_header(ui, ui_theme, "New Tag"); 34 | 35 | OverlayHelper::helper_update_spacer(ui, ui_theme); 36 | OverlayHelper::helper_update_spacer(ui, ui_theme); 37 | OverlayHelper::helper_update_tag(ui, ui_theme, &tag_data.tag.name, &tag_data.font_color, &tag_data.back_color); 38 | OverlayHelper::helper_update_spacer(ui, ui_theme); 39 | OverlayHelper::helper_update_spacer(ui, ui_theme); 40 | 41 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Main Content", true, |ui| { 42 | OverlayHelper::helper_update_adapter(ui, ui_theme, &mut tag_data.tag.adapter, &tag_data.adapters); 43 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 44 | OverlayHelper::helper_update_text(ui, ui_theme, &mut tag_data.tag.name, "Name:"); 45 | }); 46 | 47 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Appearance", false, |ui| { 48 | OverlayHelper::helper_update_color(ui, ui_theme, "Front:", &mut tag_data.font_color); 49 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 50 | OverlayHelper::helper_update_color(ui, ui_theme, "Back:", &mut tag_data.back_color); 51 | }); 52 | 53 | OverlayHelper::helper_update_small_spacer(ui, ui_theme); 54 | OverlayHelper::helper_update_errors(ui, ui_theme, &tag_data.errors); 55 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Create Tag".to_string())) { 56 | DialogOptions::Nothing => OverlayAction::Nothing, 57 | DialogOptions::Close => OverlayAction::CloseOverlay, 58 | DialogOptions::Confirm => OverlayAction::NewTag( 59 | tag_data.tag.clone() 60 | .with_hex_colors( 61 | UIController::color_as_string(tag_data.back_color).as_str(), 62 | UIController::color_as_string(tag_data.font_color).as_str())), 63 | } 64 | } 65 | 66 | } 67 | 68 | impl OverlayAction { 69 | pub(crate) fn action_tag( 70 | ui_controller: &mut UIController, 71 | cache: &mut UICache, 72 | tag: Tag 73 | ) { 74 | ui_controller.using_ticket_provider_mut(|controller, provider| { 75 | match provider.tag_validate(&tag) { 76 | Ok(_) => { 77 | match provider.tag_write(&tag) { 78 | Ok(_) => { 79 | cache.tags_valid = false; 80 | controller.close_overlay() 81 | }, 82 | Err(error) => { 83 | 84 | let error_message = error.get_text(); 85 | let mut errors = vec![("other".to_string(), error_message)]; 86 | 87 | Overlay::put_errors(&mut controller.get_current_overlay(), &mut errors); 88 | 89 | }, 90 | }; 91 | }, 92 | Err(adapter_error) => { 93 | 94 | let mut errors = match adapter_error.error_type { 95 | tickets_rs_core::AdapterErrorType::Validate(errors_vec, _) => errors_vec, 96 | _ => { 97 | let error_message = adapter_error.get_text(); 98 | vec![("other".to_string(), error_message)] 99 | } 100 | }; 101 | 102 | Overlay::put_errors(&mut controller.get_current_overlay(), &mut errors); 103 | } 104 | } 105 | }); 106 | } 107 | 108 | pub(crate) fn action_tag_delete( 109 | ui_controller: &mut UIController, 110 | cache: &mut UICache, 111 | tag: Tag 112 | ) { 113 | let mut action_successful: bool = false; 114 | ui_controller.using_ticket_provider_mut(|controller, provider| { 115 | 116 | match provider.tag_drop(&tag) { 117 | Ok(_) => action_successful = true, 118 | Err(error) => { 119 | 120 | let error_message = error.get_text(); 121 | let mut errors = vec![("other".to_string(), error_message)]; 122 | 123 | Overlay::put_errors(controller.get_current_overlay(), &mut errors); 124 | 125 | }, 126 | }; 127 | 128 | }); 129 | 130 | if action_successful { 131 | ui_controller.close_overlay(); 132 | cache.tags_valid = false; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /ui/src/overlays/overlay_wizard.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::Ui; 2 | use tickets_rs_core::Config; 3 | use std::fmt; 4 | 5 | use crate::{Overlay, UITheme, ui_controller, UIController, UICache}; 6 | 7 | use super::{OverlayHelper, OverlayAction, DialogOptions}; 8 | 9 | 10 | #[derive(Default, PartialEq, Clone)] 11 | pub struct WizardData { 12 | pub page: u8, 13 | pub extension_config: Option, 14 | pub username: String 15 | } 16 | 17 | impl Overlay { 18 | 19 | pub(crate) fn update_wizard( 20 | ui: &mut Ui, 21 | ui_theme: &mut UITheme, 22 | ui_controller: &mut UIController, 23 | wizard_data: &mut WizardData, 24 | ) -> OverlayAction { 25 | OverlayHelper::helper_update_header(ui, ui_theme, format!("Setup Wizard ({}/3)", wizard_data.page + 1u8).as_str() ); 26 | 27 | match wizard_data.page { 28 | 0 => Overlay::update_wizard_0(ui, ui_theme, ui_controller, wizard_data), 29 | 1 => Overlay::update_wizard_1(ui, ui_theme, ui_controller, wizard_data), 30 | _ => Overlay::update_wizard_2(ui, ui_theme, ui_controller, wizard_data), 31 | } 32 | 33 | if wizard_data.page < 2u8 { 34 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Next".to_string())) { 35 | DialogOptions::Nothing => OverlayAction::Nothing, 36 | DialogOptions::Close => OverlayAction::CloseOverlay, 37 | DialogOptions::Confirm => { 38 | wizard_data.page += 1; 39 | OverlayAction::Nothing 40 | }, 41 | } 42 | } else { 43 | match OverlayHelper::helper_update_dialog_buttons(ui, ui_theme, Some("Finish".to_string())) { 44 | DialogOptions::Nothing => OverlayAction::Nothing, 45 | DialogOptions::Close => OverlayAction::CloseOverlay, 46 | DialogOptions::Confirm => OverlayAction::WizardDone(wizard_data.clone()), 47 | } 48 | } 49 | 50 | } 51 | 52 | fn update_wizard_0( 53 | ui: &mut Ui, 54 | ui_theme: &UITheme, 55 | ui_controller: &mut UIController, 56 | wizard_data: &mut WizardData, 57 | ) { 58 | let label_text = 59 | ["Welcome to tickets.rs!", "", 60 | "This Wizard is here to prepare all necessary data for you to easily being able to use this tool.", 61 | "If you accidentally close this overlay, you can reopen it under Help -> Open Wizard.", "", 62 | "Let's start; What is the name, you want to use in tasks and tickets assigned to yourself?"].join("\n"); 63 | ui.label(label_text); 64 | 65 | OverlayHelper::helper_update_spacer(ui, ui_theme); 66 | 67 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "General Info", true, |ui| { 68 | OverlayHelper::helper_update_text(ui, ui_theme, &mut wizard_data.username, "Username:"); 69 | }); 70 | } 71 | 72 | fn update_wizard_1( 73 | ui: &mut Ui, 74 | mut ui_theme: &mut UITheme, 75 | ui_controller: &mut UIController, 76 | wizard_data: &mut WizardData, 77 | ) { 78 | let label_text = 79 | ["Thanks for the Name! It will be saved locally, so you don't need to worry it being uploaded somewhere", 80 | "Let's look into the appearance of the Tool next.", 81 | "Do you prefer a light theme, or a dark theme? What is your preferred Font size?"].join("\n"); 82 | ui.label(label_text); 83 | 84 | let ui_theme_copy = ui_theme.clone(); 85 | 86 | OverlayHelper::helper_update_spacer(ui, &ui_theme_copy); 87 | 88 | OverlayHelper::helper_update_section_collapsing(ui, &ui_theme_copy, "Appearance", true, |ui| { 89 | let old_font_size = ui_theme.font_size; 90 | OverlayHelper::helper_update_theme(ui, &ui_theme_copy, &mut ui_theme, ui_controller); 91 | ui_theme.font_size = old_font_size; 92 | 93 | if OverlayHelper::helper_update_number(ui, &ui_theme_copy, &mut ui_theme.font_size, "Font size:") { 94 | ui_controller.font_changed = true; 95 | }; 96 | }); 97 | 98 | OverlayHelper::helper_update_section_collapsing(ui, &ui_theme_copy, "Additional Appearance Details", false, |ui| { 99 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Primary Background", &mut ui_theme.background_primary); 100 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Secondary Background", &mut ui_theme.background_secondary); 101 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Tertiary Background", &mut ui_theme.background_tertiary); 102 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Error Background", &mut ui_theme.background_error); 103 | OverlayHelper::helper_update_spacer(ui, &ui_theme_copy); 104 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Primary Text", &mut ui_theme.foreground_primary); 105 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Secondary Text", &mut ui_theme.foreground_secondary); 106 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Tertiary Text", &mut ui_theme.foreground_tertiary); 107 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Marker Text", &mut ui_theme.foreground_marker); 108 | OverlayHelper::helper_update_color(ui, &ui_theme_copy, "Second Marker Text", &mut ui_theme.foreground_marker2); 109 | }); 110 | } 111 | 112 | fn update_wizard_2( 113 | ui: &mut Ui, 114 | ui_theme: &UITheme, 115 | ui_controller: &mut UIController, 116 | wizard_data: &mut WizardData, 117 | ) { 118 | let label_text = 119 | ["Okay, that's the appearance!", 120 | "Now let's finally set up the extensions, you want to use.", 121 | "Extensions essentially define the functionality you have within this tool, and to other interfaces.", "", 122 | "Go ahead and choose the ones you need."].join("\n"); 123 | ui.label(label_text); 124 | 125 | OverlayHelper::helper_update_spacer(ui, ui_theme); 126 | 127 | OverlayHelper::helper_update_section_collapsing(ui, ui_theme, "Extensions", true, |ui| { 128 | OverlayHelper::helper_update_extensions( 129 | ui, 130 | ui_theme, 131 | ui_controller.ticket_provider.clone(), 132 | &mut wizard_data.extension_config, 133 | ui_controller.update_panel_data.clone() 134 | ); 135 | }); 136 | } 137 | } 138 | 139 | impl OverlayAction { 140 | pub(crate) fn action_wizard( 141 | ui_controller: &mut UIController, 142 | cache: &mut UICache, 143 | wizard_data: WizardData 144 | ) { 145 | ui_controller.close_overlay(); 146 | ui_controller.invalidate_cache(Some(cache)); 147 | ui_controller.trigger_bucket_panel_update(); 148 | match ui_controller.configuration.lock() { 149 | Ok(mut config_lock) => { 150 | config_lock.put("wizard", false, ""); 151 | config_lock.put("username", wizard_data.username, ""); 152 | cache.username_valid = false; 153 | 154 | }, 155 | Err(err) => println!("Wasn't able to access app config at the end of the wizard due to {err}") 156 | }; 157 | } 158 | } -------------------------------------------------------------------------------- /ui/src/ui_cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use eframe::egui::Color32; 3 | use egui_commonmark::CommonMarkCache; 4 | use tickets_rs_core::StateIdentifier; 5 | 6 | use crate::UIController; 7 | 8 | #[derive(Default, PartialEq, Eq, Hash)] 9 | pub struct TagCacheKey { 10 | pub name: String, 11 | pub adapter: String 12 | } 13 | 14 | impl TagCacheKey { 15 | pub fn new(name: String, adapter: String) -> TagCacheKey { 16 | TagCacheKey { name: name, adapter: adapter } 17 | } 18 | } 19 | 20 | pub type TagCacheValue = [Color32; 2]; 21 | pub type TagsCache = HashMap; 22 | 23 | 24 | 25 | #[derive(Default)] 26 | pub struct UICache { 27 | pub tags_valid: bool, 28 | pub tags: TagsCache, 29 | 30 | pub states_valid: bool, 31 | pub states: HashMap, 32 | 33 | pub username_valid: bool, 34 | pub username: String, 35 | 36 | pub commonmark: CommonMarkCache 37 | } 38 | 39 | impl UICache { 40 | 41 | pub fn refresh_cache(&mut self, ui_controller: &mut UIController) { 42 | 43 | if ui_controller.invalidate_cache { 44 | self.tags_valid = false; 45 | self.states_valid = false; 46 | } 47 | 48 | self.refresh_tags(ui_controller); 49 | self.refresh_states(ui_controller); 50 | self.refresh_username(ui_controller); 51 | 52 | ui_controller.invalidate_cache = false; 53 | } 54 | 55 | pub fn refresh_username(&mut self, ui_controller: &UIController) { 56 | if !self.username_valid { 57 | self.username = match ui_controller.configuration.lock() { 58 | Ok(mut config) => config.get_or_default("username", "New User", "").raw().clone(), 59 | Err(err) => { 60 | println!("Wasn't able to lock Configuration while refreshing username, due to {err}"); 61 | self.username.clone() 62 | }, 63 | }; 64 | self.username_valid = true; 65 | } 66 | } 67 | 68 | pub fn refresh_tags(&mut self, ui_controller: &UIController) { 69 | if !self.tags_valid { 70 | self.tags = ui_controller.get_tags_cache(); 71 | self.tags_valid = true; 72 | } 73 | } 74 | 75 | pub fn refresh_states(&mut self, ui_controller: &UIController) { 76 | if !self.states_valid { 77 | self.states = ui_controller.get_states(); 78 | self.states_valid = true; 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /ui/src/ui_theme.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | use eframe::egui::Color32; 3 | use eframe::Theme; 4 | use egui::{hex_color, epaint}; 5 | use tickets_rs_core::AppConfig; 6 | 7 | #[derive(Clone)] 8 | pub struct UITheme { 9 | pub base_theme: Theme, 10 | pub background_primary: Color32, 11 | pub background_secondary: Color32, 12 | pub background_tertiary: Color32, 13 | pub background_error: Color32, 14 | pub foreground_primary: Color32, 15 | pub foreground_secondary: Color32, 16 | pub foreground_tertiary: Color32, 17 | pub foreground_marker: Color32, 18 | pub foreground_marker2: Color32, 19 | pub font_size: i32, 20 | pub config: Option>>, 21 | } 22 | 23 | impl PartialEq for UITheme { 24 | fn eq(&self, other: &Self) -> bool { 25 | self.base_theme == other.base_theme && 26 | 27 | self.background_primary == other.background_primary && 28 | self.background_secondary == other.background_secondary && 29 | self.background_tertiary == other.background_tertiary && 30 | self.background_error == other.background_error && 31 | 32 | self.foreground_primary == other.foreground_primary && 33 | self.foreground_secondary == other.foreground_secondary && 34 | self.foreground_tertiary == other.foreground_tertiary && 35 | self.foreground_marker == other.foreground_marker && 36 | self.foreground_marker2 == other.foreground_marker2 37 | } 38 | } 39 | 40 | impl Default for UITheme { 41 | fn default() -> Self { 42 | UITheme::theme_dark() 43 | } 44 | } 45 | 46 | impl Drop for UITheme { 47 | fn drop(&mut self) { 48 | 49 | match &self.config { 50 | Some(config_mutex) => { 51 | self.write(config_mutex.clone()) 52 | }, 53 | None => (), 54 | }; 55 | } 56 | } 57 | 58 | impl UITheme { 59 | 60 | pub fn theme_dark() -> UITheme { 61 | UITheme { 62 | base_theme: Theme::Dark, 63 | background_primary: Color32::from_rgba_unmultiplied(0x1a, 0x1a, 0x1a, 0xff), 64 | background_secondary: Color32::from_rgba_unmultiplied(0x25, 0x25, 0x25, 0xff), 65 | background_tertiary: Color32::from_rgba_unmultiplied(0x2d, 0x2d, 0x2d, 0xff), 66 | background_error: Color32::from_rgba_unmultiplied(0x38, 0x18, 0x18, 0xff), 67 | foreground_primary: Color32::from_rgba_unmultiplied(0xcc, 0xcc, 0xcc, 0xff), 68 | foreground_secondary: Color32::from_rgba_unmultiplied(0x80, 0x80, 0x80, 0xff), 69 | foreground_tertiary: Color32::from_rgba_unmultiplied(0xf5, 0xf5, 0xf5, 0xff), 70 | foreground_marker: Color32::from_rgba_unmultiplied(0x89, 0x66, 0x10, 0xff), 71 | foreground_marker2: Color32::from_rgba_unmultiplied(0xda, 0xa5, 0x20, 0xff), 72 | font_size: 13, 73 | config: None, 74 | } 75 | } 76 | 77 | pub fn theme_light() -> UITheme { 78 | UITheme { 79 | base_theme: Theme::Light, 80 | background_primary: Color32::from_rgba_unmultiplied(0xe0, 0xe0, 0xe0, 0xff), 81 | background_secondary: Color32::from_rgba_unmultiplied(0xb8, 0xb8, 0xb8, 0xff), 82 | background_tertiary: Color32::from_rgba_unmultiplied(0x92, 0x92, 0x92, 0xff), 83 | background_error: Color32::from_rgba_unmultiplied(0xfb, 0x98, 0x97, 0xff), 84 | foreground_primary: Color32::from_rgba_unmultiplied(0x2d, 0x2d, 0x2d, 0xff), 85 | foreground_secondary: Color32::from_rgba_unmultiplied(0x51, 0x51, 0x51, 0xff), 86 | foreground_tertiary: Color32::from_rgba_unmultiplied(0x13, 0x13, 0x13, 0xff), 87 | foreground_marker: Color32::from_rgba_unmultiplied(0x66, 0x4f, 0x17, 0xff), 88 | foreground_marker2: Color32::from_rgba_unmultiplied(0x2b, 0x20, 0x04, 0xff), 89 | font_size: 13, 90 | config: None, 91 | } 92 | } 93 | 94 | pub fn name(&self) -> String { 95 | 96 | if *self == UITheme::theme_light() { 97 | return "Light Theme".to_string(); 98 | } 99 | 100 | if *self == UITheme::theme_dark() { 101 | return "Dark Theme".to_string(); 102 | } 103 | 104 | "Custom Theme".to_string() 105 | } 106 | 107 | pub fn names() -> Vec { 108 | vec![ 109 | "Light Theme".to_string(), 110 | "Dark Theme".to_string() 111 | ] 112 | } 113 | 114 | pub fn from_name(name: &str) -> UITheme { 115 | 116 | if name == "Light Theme" { 117 | return UITheme::theme_light(); 118 | } 119 | 120 | if name == "Dark Theme" { 121 | return UITheme::theme_dark(); 122 | } 123 | 124 | UITheme::default() 125 | } 126 | 127 | pub fn with_config(mut self, config: Option>>) -> Self { 128 | self.config = config; 129 | self 130 | } 131 | 132 | pub fn from(config_mutex: Arc>) -> UITheme { 133 | 134 | match config_mutex.lock() { 135 | Ok(mut config) => { 136 | UITheme { 137 | 138 | config: Some(config_mutex.clone()), 139 | 140 | base_theme: { 141 | if config.get_or_default( 142 | "theme:base", 143 | "dark", "").raw().to_ascii_lowercase() == *"dark" { 144 | Theme::Dark 145 | } else { 146 | Theme::Light 147 | } 148 | }, 149 | 150 | background_primary: config.get_or_default( 151 | "color:background:primary", 152 | "#1a1a1aff", 153 | "").get().unwrap(), 154 | 155 | background_secondary: config.get_or_default( 156 | "color:background:secondary", 157 | "#252525ff", 158 | "").get().unwrap(), 159 | 160 | background_tertiary: config.get_or_default( 161 | "color:background:tertiary", 162 | "#2d2d2dff", 163 | "").get().unwrap(), 164 | 165 | background_error: config.get_or_default( 166 | "color:background:error", 167 | "#381818ff", 168 | "").get().unwrap(), 169 | 170 | foreground_primary: config.get_or_default( 171 | "color:foreground:primary", 172 | "#ccccccff", 173 | "").get().unwrap(), 174 | 175 | foreground_secondary: config.get_or_default( 176 | "color:foreground:secondary", 177 | "#808080ff", 178 | "").get().unwrap(), 179 | 180 | foreground_tertiary: config.get_or_default( 181 | "color:foreground:tertiary", 182 | "#f5f5f5ff", 183 | "").get().unwrap(), 184 | 185 | foreground_marker: config.get_or_default( 186 | "color:foreground:marker", 187 | "#896610ff", 188 | "").get().unwrap(), 189 | 190 | foreground_marker2: config.get_or_default( 191 | "color:foreground:marker2", 192 | "#daa520ff", 193 | "").get().unwrap(), 194 | 195 | font_size: config.get_or_default( 196 | "font:size", 197 | 13, 198 | "").get().unwrap() 199 | } 200 | }, 201 | Err(err) => { 202 | println!("Wasn't able to lock configuration! reason: {}", err); 203 | UITheme::theme_dark() 204 | }, 205 | } 206 | } 207 | 208 | pub fn get(&self, name: String) -> Color32 { 209 | match name.as_str() { 210 | "color:background:primary" => self.background_primary, 211 | "color:background:secondary" => self.background_secondary, 212 | "color:background:tertiary" => self.background_tertiary, 213 | "color:background:error" => self.background_error, 214 | "color:foreground:primary" => self.foreground_primary, 215 | "color:foreground:secondary" => self.foreground_secondary, 216 | "color:foreground:tertiary" => self.foreground_tertiary, 217 | "color:foreground:marker" => self.foreground_marker, 218 | "color:foreground:marker2" => self.foreground_marker2, 219 | _ => Color32::WHITE 220 | } 221 | } 222 | 223 | pub fn merge_colors(&mut self, other_theme: &UITheme) { 224 | self.base_theme = other_theme.base_theme; 225 | self.background_primary = other_theme.background_primary; 226 | self.background_secondary = other_theme.background_secondary; 227 | self.background_tertiary = other_theme.background_tertiary; 228 | self.background_error = other_theme.background_error; 229 | self.foreground_primary = other_theme.foreground_primary; 230 | self.foreground_secondary = other_theme.foreground_secondary; 231 | self.foreground_tertiary = other_theme.foreground_tertiary; 232 | self.foreground_marker = other_theme.foreground_marker; 233 | self.foreground_marker2 = other_theme.foreground_marker2; 234 | } 235 | 236 | fn write(&self, config: Arc>) { 237 | match config.lock() { 238 | Ok(mut lock) => { 239 | let base_theme_name = if self.base_theme == Theme::Dark {"dark"} else {"light"}; 240 | lock.put("theme:base", base_theme_name, ""); 241 | 242 | lock.put("color:background:primary", self.background_primary, ""); 243 | lock.put("color:background:secondary", self.background_secondary, ""); 244 | lock.put("color:background:tertiary", self.background_tertiary, ""); 245 | lock.put("color:background:error", self.background_error, ""); 246 | 247 | lock.put("color:foreground:primary", self.foreground_primary, ""); 248 | lock.put("color:foreground:secondary", self.foreground_secondary, ""); 249 | lock.put("color:foreground:tertiary", self.foreground_tertiary, ""); 250 | lock.put("color:foreground:marker", self.foreground_marker, ""); 251 | lock.put("color:foreground:marker2", self.foreground_marker2, ""); 252 | 253 | lock.put("font:size", self.font_size, ""); 254 | 255 | }, 256 | Err(err) => println!("Wasn't able to lock Configuration, due to {}", err), 257 | } 258 | } 259 | } -------------------------------------------------------------------------------- /ui/src/user_interface/menu_bar.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{ 2 | Rounding, 3 | style::Margin, 4 | Stroke, 5 | Frame, 6 | Ui, 7 | menu 8 | }; 9 | 10 | use crate::{ 11 | UserInterface, 12 | ui_controller::TicketViewMode, 13 | Overlay, 14 | overlays::WizardData 15 | }; 16 | 17 | 18 | impl UserInterface { 19 | 20 | pub fn update_menu_bar(&mut self, ctx: &egui::Context, ui: &mut Ui) { 21 | let frame = Frame { 22 | inner_margin: Margin::same(8.0), 23 | outer_margin: Margin::same(0.0), 24 | rounding: Rounding::same(0.0), 25 | shadow: ctx.style().visuals.popup_shadow, 26 | fill: ctx.style().visuals.window_fill(), 27 | stroke: Stroke::none(), 28 | }; 29 | 30 | menu::bar(ui, |ui| { 31 | ui.menu_button("File", |ui| { 32 | 33 | // Setup the menu internals first 34 | let button_new_ticket = ui.button("New Ticket") 35 | .on_hover_text_at_pointer("Shows a Dialog Window for creating a new Ticket"); 36 | 37 | let button_import_ticket = ui.button("Import Ticket") 38 | .on_hover_text_at_pointer("Imports a previously exported Ticket into tickets.rs"); 39 | 40 | let button_add_tickets = ui.button("Add Tickets...") 41 | .on_hover_text_at_pointer("Shows Dialog Window for adding Tickets in bulk"); 42 | 43 | ui.separator(); 44 | 45 | let button_new_bucket = ui.button("New Bucket") 46 | .on_hover_text_at_pointer("Shows a Dialog Window for creating a new Bucket"); 47 | 48 | let button_import_bucket = ui.button("Import Bucket") 49 | .on_hover_text_at_pointer("Imports a previously exported Bucket into tickets.rs"); 50 | 51 | ui.separator(); 52 | 53 | let button_exit = ui.button("Exit") 54 | .on_hover_text_at_pointer("Exits tickets.rs"); 55 | 56 | // Events Handling 57 | if button_new_ticket.clicked() { 58 | self.ui_controller.open_overlay(self.ui_controller.create_new_ticket_overlay(None)); 59 | ui.close_menu(); 60 | } 61 | 62 | if button_import_ticket.clicked() { 63 | ui.close_menu(); 64 | } 65 | 66 | if button_add_tickets.clicked() { 67 | ui.close_menu(); 68 | } 69 | 70 | if button_new_bucket.clicked() { 71 | self.ui_controller.open_overlay(self.ui_controller.create_new_bucket_overlay(None)); 72 | ui.close_menu(); 73 | } 74 | 75 | if button_import_bucket.clicked() { 76 | ui.close_menu(); 77 | } 78 | 79 | if button_exit.clicked() { 80 | self.ui_controller.running = false; 81 | ui.close_menu(); 82 | } 83 | 84 | }); 85 | 86 | ui.menu_button("Edit", |ui| { 87 | 88 | // Setup the menu points itself 89 | let button_refresh = ui.button("Refresh") 90 | .on_hover_text_at_pointer("Pulls in the currently displayed data in again."); 91 | 92 | let button_preferences = ui.button("Preferences...") 93 | .on_hover_text_at_pointer("Shows a Dialog Window changing settings regarding tickets.rs"); 94 | 95 | // Handle the Events 96 | if button_refresh.clicked() { 97 | self.ui_controller.trigger_bucket_panel_update(); 98 | ui.close_menu(); 99 | } 100 | 101 | if button_preferences.clicked() { 102 | self.ui_controller.open_overlay(self.ui_controller.create_preferences_overlay()); 103 | ui.close_menu(); 104 | } 105 | }); 106 | 107 | ui.menu_button("View", |ui| { 108 | 109 | let choice_regular = ui.radio_value(&mut self.ui_controller.ticket_view_mode, TicketViewMode::Regular, "Display Tickets full size") 110 | .on_hover_text_at_pointer("Do the tickets need to be displayed in full size? If yes, then click this option."); 111 | 112 | let choice_half = ui.radio_value(&mut self.ui_controller.ticket_view_mode, TicketViewMode::Half, "Display Tickets half size") 113 | .on_hover_text_at_pointer("If you want to see more Tickets at once, but don't want to give up the Description, this setting is for you."); 114 | 115 | let choice_list = ui.radio_value(&mut self.ui_controller.ticket_view_mode, TicketViewMode::List, "Display Tickets as list") 116 | .on_hover_text_at_pointer("For a quick overview, you can switch the ticket display to a compact list."); 117 | 118 | ui.separator(); 119 | 120 | let checkbox_sidebar = ui.checkbox(&mut self.ui_controller.show_sidebar, "Show Sidebar") 121 | .on_hover_text_at_pointer("Toggles, if the Sidebar, that shows Filters and Adapters is hidden, or not."); 122 | 123 | 124 | if choice_regular.clicked() || 125 | choice_half.clicked() || 126 | choice_list.clicked() { 127 | 128 | match self.ui_controller.configuration.lock(){ 129 | Ok(mut lock) => { 130 | match self.ui_controller.ticket_view_mode { 131 | TicketViewMode::Regular => lock.put("ticket:view_mode", "regular", ""), 132 | TicketViewMode::Half => lock.put("ticket:view_mode", "half", ""), 133 | TicketViewMode::List => lock.put("ticket:view_mode", "list", ""), 134 | } 135 | }, 136 | Err(err) => println!("Wasn't able to lock Config, when pushing a view Button, due to {err}"), 137 | } 138 | 139 | 140 | ui.close_menu(); 141 | }; 142 | 143 | if checkbox_sidebar.clicked() { 144 | 145 | match self.ui_controller.configuration.lock(){ 146 | Ok(mut lock) => { 147 | lock.put("sidebar:enabled", self.ui_controller.show_sidebar, ""); 148 | }, 149 | Err(err) => println!("Wasn't able to lock Config, when pushing a view Button, due to {err}"), 150 | } 151 | 152 | ui.close_menu(); 153 | } 154 | }); 155 | 156 | 157 | ui.menu_button("Help", |ui| { 158 | 159 | let button_wizard = ui.button("Reopen Wizard...") 160 | .on_hover_text_at_pointer("Just in case you accidentally closed it, you can reopen the Wizard here again."); 161 | 162 | let button_about = ui.button("About tickets.rs") 163 | .on_hover_text_at_pointer("Show some information about the Program and it's contributors."); 164 | 165 | if button_about.clicked() { 166 | self.ui_controller.open_overlay(Overlay::About); 167 | ui.close_menu(); 168 | } 169 | 170 | if button_wizard.clicked() { 171 | 172 | let mut username = "New User".to_string(); 173 | match self.ui_controller.configuration.lock() { 174 | Ok(mut lock) => username = lock.get_or_default("username", "New User", "").raw().clone(), 175 | Err(err) => println!("Wasn't able to lock App Config for Wizard due to {err}"), 176 | } 177 | 178 | self.ui_controller.open_overlay(Overlay::Wizard(WizardData{ 179 | username, 180 | ..Default::default() 181 | })); 182 | ui.close_menu(); 183 | } 184 | }); 185 | }); 186 | } 187 | 188 | } -------------------------------------------------------------------------------- /ui/src/user_interface/mod.rs: -------------------------------------------------------------------------------- 1 | mod side_panel; 2 | mod menu_bar; 3 | mod ticket; 4 | 5 | use std::{ 6 | sync::Arc, 7 | collections::HashMap, 8 | path::Path, 9 | ops::Deref 10 | }; 11 | 12 | use eframe::HardwareAcceleration; 13 | use eframe::egui::{ 14 | Vec2, 15 | Button, 16 | style::Margin, 17 | Ui, 18 | TopBottomPanel, 19 | Frame, 20 | Rounding, 21 | Stroke, 22 | SidePanel, 23 | ScrollArea, 24 | Align, 25 | CentralPanel, 26 | TextureHandle, 27 | ColorImage, 28 | FontId, 29 | FontFamily, 30 | Area, 31 | Align2, 32 | Layout, 33 | TextStyle, Context, Color32, epaint::Shadow 34 | }; 35 | 36 | use crate::{ 37 | UIController, 38 | UITheme, 39 | Overlay, overlays::OverlayAction, UICache 40 | }; 41 | 42 | pub use side_panel::SidePanelAction; 43 | 44 | 45 | pub struct UserInterface { 46 | ui_controller: UIController, 47 | cache: UICache, 48 | ui_theme: UITheme, 49 | icons: HashMap>, 50 | icon_textures: HashMap>, 51 | } 52 | 53 | impl UserInterface { 54 | 55 | pub fn launch(ui_controller: UIController, ui_theme: UITheme) { 56 | 57 | // read the application icon, if possible 58 | let icon_image = ui_controller.read_image_data_from_path(Path::new("assets/icon_app.png")); 59 | let icon = match icon_image { 60 | Some(image) => { 61 | Some(UIController::image_data_as_icon(image)) 62 | }, 63 | None => None 64 | }; 65 | 66 | let options = eframe::NativeOptions { 67 | always_on_top: false, 68 | maximized: false, 69 | decorated: true, 70 | fullscreen: false, 71 | drag_and_drop_support: false, 72 | icon_data: icon, 73 | //icon_data: None, 74 | initial_window_pos: None, 75 | initial_window_size: Some(eframe::egui::Vec2{x: 800.0, y: 600.0}), 76 | min_window_size: Some(eframe::egui::Vec2{x: 400.0, y: 200.0}), 77 | max_window_size: None, 78 | resizable: true, 79 | transparent: false, 80 | vsync: false, 81 | multisampling: 0, 82 | depth_buffer: 0, 83 | stencil_buffer: 0, 84 | hardware_acceleration: HardwareAcceleration::Preferred, 85 | renderer: eframe::Renderer::Glow, 86 | follow_system_theme: false, 87 | default_theme: ui_theme.base_theme, 88 | run_and_return: true, 89 | mouse_passthrough: false, 90 | active: true, 91 | centered: true, 92 | app_id: Some("ticket-rs".into()), 93 | persist_window: true, 94 | ..Default::default() 95 | }; 96 | 97 | eframe::run_native( 98 | "tickets.rs - A ticket Management App", 99 | options, 100 | Box::new(|_cc| Box::new(UserInterface::new(ui_controller, ui_theme))), 101 | ); 102 | } 103 | 104 | pub fn load_texture(icon_textures: & mut HashMap>, icons: &mut HashMap>, ui: &Ui, adapter_name: &String) -> Option { 105 | 106 | match icon_textures.get_mut(adapter_name) { 107 | Some(texture) => { 108 | let color_image = icons.get(adapter_name).unwrap(); 109 | match color_image { 110 | Some(found_image) => { 111 | Some(texture.get_or_insert_with(|| { 112 | ui.ctx().load_texture( 113 | adapter_name, 114 | found_image.clone(), 115 | egui::TextureOptions::LINEAR 116 | ) 117 | })).cloned() 118 | }, 119 | None => None 120 | } 121 | }, 122 | None => { 123 | match icons.get(adapter_name) { 124 | Some(found_icon_data_option) => { 125 | match found_icon_data_option { 126 | Some(found_icon_data) => { 127 | icon_textures.insert(adapter_name.clone(), None); 128 | //let mut local_tex: Option = None; 129 | let texture = icon_textures.get_mut(adapter_name).unwrap(); 130 | //let texture = &mut local_tex; 131 | Some(texture.get_or_insert_with(|| { 132 | ui.ctx().load_texture( 133 | adapter_name, 134 | found_icon_data.clone(), 135 | egui::TextureOptions::LINEAR 136 | ) 137 | })).cloned() 138 | }, 139 | None => None 140 | } 141 | }, 142 | None => None, 143 | } 144 | }, 145 | } 146 | } 147 | 148 | fn new(ui_controller: UIController, ui_theme: UITheme) -> Self { 149 | 150 | let mut icons: HashMap> = HashMap::new(); 151 | 152 | ui_controller.read_adapter_icons(&mut icons); 153 | ui_controller.read_custom_icon(&mut icons, Path::new("assets/icon_app.png"), "icon_app".into()); 154 | 155 | UserInterface { 156 | icons, 157 | icon_textures: HashMap::new(), 158 | cache: UICache::default(), 159 | ui_controller, 160 | ui_theme, 161 | } 162 | } 163 | } 164 | 165 | impl eframe::App for UserInterface { 166 | 167 | fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { 168 | 169 | if !self.ui_controller.running { 170 | self.ui_controller.on_close_ui(&self.ui_theme, frame); 171 | }; 172 | 173 | self.cache.refresh_cache(&mut self.ui_controller); 174 | 175 | let no_color = Color32::from_rgba_unmultiplied(0, 0, 0, 0); 176 | let no_shadow = Shadow { extrusion: 0.0, color: no_color }; 177 | let divider_color = Color32::from_rgba_unmultiplied( 178 | self.ui_theme.background_primary.r(), 179 | self.ui_theme.background_primary.g(), 180 | self.ui_theme.background_primary.b(), 181 | self.ui_theme.background_primary.a() / 15 * 14); 182 | 183 | if self.ui_controller.font_changed { 184 | let mut font_update_style = ctx.style().deref().clone(); 185 | 186 | font_update_style.text_styles.clear(); 187 | font_update_style.text_styles.insert(TextStyle::Body, FontId { size: self.ui_theme.font_size as f32, family: FontFamily::Proportional }); 188 | font_update_style.text_styles.insert(TextStyle::Monospace, FontId { size: self.ui_theme.font_size as f32, family: FontFamily::Monospace }); 189 | font_update_style.text_styles.insert(TextStyle::Heading, FontId { size: self.ui_theme.font_size as f32 * 1.5, family: FontFamily::Proportional }); 190 | font_update_style.text_styles.insert(TextStyle::Small, FontId { size: self.ui_theme.font_size as f32 * 0.5, family: FontFamily::Proportional }); 191 | font_update_style.text_styles.insert(TextStyle::Button, FontId { size: self.ui_theme.font_size as f32, family: FontFamily::Proportional }); 192 | ctx.set_style(Arc::new(font_update_style)); 193 | self.ui_controller.font_changed = false; 194 | } 195 | 196 | Area::new("background_area") 197 | .order(egui::Order::Background) 198 | .interactable(false) 199 | .anchor(Align2::LEFT_TOP, Vec2{x: 0.0, y: 0.0}) 200 | .show(ctx, |ui| { 201 | CentralPanel::default() 202 | .frame(Frame { 203 | inner_margin: Margin::same(0.0), 204 | outer_margin: Margin::same(0.0), 205 | rounding: Rounding::same(0.0), 206 | shadow: no_shadow, 207 | fill: self.ui_theme.background_secondary, 208 | stroke: Stroke::none(), 209 | }) 210 | .show_inside(ui, |_| {}) 211 | }); 212 | 213 | Area::new("main_area") 214 | .order(egui::Order::Background) 215 | .interactable(!self.ui_controller.has_overlay()) 216 | .anchor(Align2::LEFT_TOP, Vec2{x: 0.0, y: 0.0}) 217 | .show(ctx, |ui| { 218 | 219 | TopBottomPanel::top("menu_panel") 220 | .frame(Frame { 221 | inner_margin: Margin{ left: 2.0, right: 2.0, top: 2.0, bottom: 0.0 }, 222 | outer_margin: Margin::same(0.0), 223 | rounding: Rounding::same(0.0), 224 | shadow: no_shadow, 225 | fill: self.ui_theme.background_primary, 226 | stroke: Stroke::none(), 227 | }) 228 | .show_inside(ui, |ui| { 229 | self.update_menu_bar(ctx, ui); 230 | }); 231 | 232 | TopBottomPanel::top("menu_panel_divider") 233 | .min_height(6.0) 234 | .max_height(6.0) 235 | .frame(Frame { 236 | inner_margin: Margin::same(0.0), 237 | outer_margin: Margin::same(0.0), 238 | rounding: Rounding::same(0.0), 239 | shadow: no_shadow, 240 | fill: self.ui_theme.background_secondary, 241 | stroke: Stroke::none(), 242 | }) 243 | .show_inside(ui, |ui| {}); 244 | 245 | if self.ui_controller.show_sidebar { 246 | SidePanel::left("buckets_panel") 247 | .default_width(250.0) 248 | .min_width(100.0) 249 | .max_width((350.0_f32).min(ui.available_width() - 200.0)) 250 | .frame(Frame { 251 | inner_margin: Margin{ left: 12.0, right: 4.0, top: 2.0, bottom: 2.0 }, 252 | outer_margin: Margin::same(0.0), 253 | rounding: Rounding::same(0.0), 254 | shadow: no_shadow, 255 | fill: self.ui_theme.background_secondary, 256 | stroke: Stroke::none(), 257 | }) 258 | .show_inside(ui, |ui| { 259 | self.update_side_panel(ctx, ui) 260 | }); 261 | } else { 262 | SidePanel::left("buckets_panel") 263 | .resizable(false) 264 | .min_width(5.0) 265 | .max_width(5.0) 266 | .frame(Frame { 267 | inner_margin: Margin::same(0.0), 268 | outer_margin: Margin::same(0.0), 269 | rounding: Rounding::same(0.0), 270 | shadow: no_shadow, 271 | fill: self.ui_theme.background_secondary, 272 | stroke: Stroke::none(), 273 | }) 274 | .show_inside(ui, |ui| {}); 275 | } 276 | 277 | CentralPanel::default() 278 | .frame(Frame { 279 | inner_margin: Margin { left: 8.0, right: 2.0, top: 2.0, bottom: 2.0 }, 280 | outer_margin: Margin::same(0.0), 281 | rounding: Rounding::same(0.0), 282 | shadow: no_shadow, 283 | fill: self.ui_theme.background_primary, 284 | stroke: Stroke::none(), 285 | }) 286 | .show_inside(ui, |ui| { 287 | 288 | ScrollArea::vertical() 289 | .show(ui, |ui| { 290 | let width = ui.available_width(); 291 | ui.set_min_width(width); 292 | ui.set_max_width(width); 293 | ui.add_space(self.ui_theme.font_size as f32 / 2.0); 294 | self.ui_controller.update_each_ticket( 295 | ui, 296 | &mut self.icon_textures, 297 | &mut self.icons, 298 | &self.ui_theme, 299 | &mut self.cache 300 | ); 301 | //ui.separator(); 302 | 303 | }); 304 | 305 | }); 306 | }); 307 | 308 | let mut overlay_width = 0.0; 309 | 310 | if self.ui_controller.has_overlay() { 311 | Area::new("layer_divider") 312 | .order(egui::Order::Middle) 313 | .interactable(true) 314 | .anchor(Align2::LEFT_TOP, Vec2{x: 0.0, y: 0.0}) 315 | .show(ctx, |ui| { 316 | overlay_width = ui.available_width(); 317 | let space = Button::new("").fill(divider_color).stroke(Stroke{ width: 0.0, color: divider_color }); 318 | if ui.add_sized(ui.available_size(), space).clicked() { 319 | self.ui_controller.close_overlay(); 320 | } 321 | }); 322 | 323 | Area::new("overlay_area") 324 | .order(egui::Order::Foreground) 325 | .interactable(true) 326 | .anchor(Align2::CENTER_CENTER, Vec2{x: 0.0, y: 0.0}) 327 | .show(ctx, |ui| { 328 | ui.set_max_size(Vec2{x: (overlay_width - 64.0).min((self.ui_theme.font_size * 60) as f32), y: ui.available_height() - 64.0}); 329 | ui.set_min_size(Vec2{x: 400.0 - 32.0, y: 200.0 - 32.0}); 330 | 331 | let mut overlay_group = Frame::group(ui.style()); 332 | overlay_group = overlay_group.fill(self.ui_theme.background_secondary); 333 | overlay_group = overlay_group.shadow(ctx.style().visuals.popup_shadow); 334 | overlay_group = overlay_group.inner_margin(Margin::same(self.ui_theme.font_size as f32)); 335 | overlay_group.show(ui, |ui| { 336 | 337 | ScrollArea::new([false, true]) 338 | .auto_shrink([false, true]) 339 | .show(ui, |ui| { 340 | ui.with_layout(Layout::top_down(Align::Center).with_cross_justify(false), |ui| { 341 | ui.style_mut().spacing.item_spacing.y = self.ui_theme.font_size as f32 / 2.0; 342 | let mut overlay = self.ui_controller.get_current_overlay().clone(); 343 | let action = Overlay::update( 344 | &mut overlay, 345 | ui, 346 | &mut self.ui_theme, 347 | &mut self.ui_controller, 348 | &mut self.cache, 349 | &mut self.icon_textures, 350 | &mut self.icons 351 | ); 352 | 353 | self.ui_controller.open_overlay(overlay); 354 | 355 | if action != OverlayAction::Nothing { 356 | action.execute(&mut self.ui_controller, &mut self.cache) 357 | }; 358 | }); 359 | }); 360 | }); 361 | }); 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /ui/src/user_interface/side_panel.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{ 2 | Ui, 3 | SelectableLabel, 4 | Align, Label, 5 | RichText, 6 | Button, 7 | ScrollArea}; 8 | use tickets_rs_core::FilterType; 9 | 10 | use crate::{ 11 | UserInterface, 12 | UITheme, 13 | ui_controller::{ 14 | BucketPanelFolder, 15 | BucketPanelEntry 16 | } 17 | }; 18 | 19 | pub enum SidePanelAction { 20 | FolderOpenClose, 21 | FolderClicked, 22 | FolderNewTag, 23 | FolderNewBucket, 24 | FolderNewFilter, 25 | FolderNewTicket, 26 | FolderNewState, 27 | FolderRemove, 28 | EntryClicked, 29 | EntryRemove, 30 | EntryEdit, 31 | EntryBucketRemove(u64), 32 | Nothing, 33 | } 34 | 35 | impl UserInterface { 36 | 37 | pub(crate) fn update_side_panel_folder(ui: &mut Ui, ui_theme: &UITheme, is_selected: bool, is_open: bool, selectable: bool, folder: &BucketPanelFolder) -> SidePanelAction { 38 | 39 | let mut action: SidePanelAction = SidePanelAction::Nothing; 40 | 41 | ui.with_layout(egui::Layout::left_to_right(Align::LEFT), |ui| { 42 | 43 | if ui.add_sized([16.0, 16.0], SelectableLabel::new(false, if is_open {"⊟"} else {"⊞"})).clicked() { 44 | action = SidePanelAction::FolderOpenClose; 45 | } 46 | 47 | 48 | 49 | if selectable { 50 | ui.add_sized([4.0, 16.0], Label::new("")); 51 | let button = SelectableLabel::new(is_selected, RichText::new(&folder.label).color(ui_theme.foreground_marker2)); 52 | let mut response = ui.add(button); 53 | 54 | if response.clicked() { 55 | action = SidePanelAction::FolderClicked; 56 | }; 57 | 58 | response.context_menu(|ui| { 59 | 60 | if ui.button("Add Ticket").clicked() { 61 | ui.close_menu(); 62 | action = SidePanelAction::FolderNewTicket; 63 | }; 64 | 65 | if ui.button("Add Bucket").clicked() { 66 | ui.close_menu(); 67 | action = SidePanelAction::FolderNewBucket; 68 | }; 69 | 70 | if ui.button("Add Tag").clicked() { 71 | ui.close_menu(); 72 | action = SidePanelAction::FolderNewTag; 73 | }; 74 | 75 | if ui.button("Add State").clicked() { 76 | ui.close_menu(); 77 | action = SidePanelAction::FolderNewState; 78 | }; 79 | 80 | if ui.button("Add Custom Filter").clicked() { 81 | ui.close_menu(); 82 | action = SidePanelAction::FolderNewFilter 83 | }; 84 | 85 | ui.separator(); 86 | 87 | if ui.button(RichText::new("Remove this Adapter").color(ui_theme.foreground_marker2)).clicked() { 88 | ui.close_menu(); 89 | action = SidePanelAction::FolderRemove 90 | }; 91 | }); 92 | 93 | } else { 94 | ui.add_sized([8.0, 16.0], Label::new("")); 95 | let label = Label::new(RichText::new(&folder.label).color(ui_theme.foreground_marker)); 96 | 97 | let response = ui.add(label); 98 | 99 | if response.clicked() { 100 | action = SidePanelAction::FolderClicked; 101 | }; 102 | 103 | response.context_menu(|ui| { 104 | 105 | if ui.button("Add Custom Filter").clicked() { 106 | ui.close_menu(); 107 | action = SidePanelAction::FolderNewFilter 108 | }; 109 | 110 | }); 111 | 112 | 113 | }; 114 | 115 | action 116 | 117 | }).inner 118 | } 119 | 120 | pub(crate) fn update_side_panel_entry(ui: &mut Ui, ui_theme: &UITheme, is_selected: bool, entry: &BucketPanelEntry) -> SidePanelAction { 121 | 122 | let mut action = SidePanelAction::Nothing; 123 | 124 | let entry_icon = match entry.entry_type { 125 | FilterType::User => "⚙", 126 | FilterType::Builtin => "🔨", 127 | FilterType::Bucket(_) => "🗄", 128 | FilterType::Tag => "🏷", 129 | FilterType::Other => "❔", 130 | }; 131 | 132 | ui.with_layout(egui::Layout::left_to_right(Align::LEFT), |ui| { 133 | 134 | ui.add_sized([24.0, 16.0], Label::new("")); 135 | ui.add_sized([16.0, 16.0], Label::new(entry_icon)); 136 | 137 | let button = SelectableLabel::new(is_selected, RichText::new(&entry.label).color(ui_theme.foreground_secondary).italics()); 138 | let mut response = ui.add(button); 139 | 140 | match entry.entry_type { 141 | FilterType::User => { 142 | response = response.context_menu(|ui| { 143 | if ui.button("Edit").clicked() { 144 | action = SidePanelAction::EntryEdit; 145 | ui.close_menu(); 146 | }; 147 | 148 | if ui.button(RichText::new("Remove").color(ui_theme.foreground_marker2)).clicked() { 149 | action = SidePanelAction::EntryRemove; 150 | ui.close_menu(); 151 | }; 152 | }); 153 | }, 154 | FilterType::Bucket(id) => { 155 | response = response.context_menu(|ui| { 156 | 157 | if ui.button(RichText::new("Remove").color(ui_theme.foreground_marker2)).clicked() { 158 | action = SidePanelAction::EntryBucketRemove(id); 159 | ui.close_menu(); 160 | }; 161 | }); 162 | }, 163 | _ => () 164 | 165 | } 166 | 167 | 168 | if response.clicked() { 169 | action = SidePanelAction::EntryClicked 170 | } 171 | }); 172 | 173 | action 174 | } 175 | 176 | pub(crate) fn update_side_panel_space(ui: &mut Ui) -> bool { 177 | let space = Button::new("").frame(false); 178 | ui.add_sized([ui.available_width(), ui.available_height()], space).double_clicked() 179 | } 180 | 181 | pub(crate) fn update_side_panel(&mut self, ctx: &egui::Context, ui: &mut Ui) { 182 | 183 | let shift_or_ctrl = ctx.input(|i| i.modifiers.ctrl || i.modifiers.shift); 184 | let controller = &mut self.ui_controller; 185 | let ui_theme = &self.ui_theme; 186 | 187 | controller.check_bucket_panel_trigger(&mut self.cache); 188 | 189 | ScrollArea::vertical().show(ui, |ui| { 190 | 191 | ui.spacing_mut().item_spacing.x = 0.0; 192 | 193 | match controller.update_each_folder(ui, ui_theme) { 194 | Some(folder) => { 195 | controller.toggle_folder_in_panel(folder, shift_or_ctrl); 196 | controller.execute_bucket_panel_selection(); 197 | }, 198 | None => (), 199 | }; 200 | }); 201 | } 202 | 203 | } --------------------------------------------------------------------------------