[READ-ONLY] Mirror of https://github.com/probablykasper/remind-me-again. Toggleable cron reminders app for Mac, Linux and Windows
linux macos notifications reminder tauri windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Persist app data

Kasper (Jul 12, 2022, 12:19 AM +0200) f8f46d3e 2ac5e6a4

+145 -32
+1
.gitignore
··· 4 4 5 5 src-tauri/target 6 6 src-tauri/WixTools 7 + src-tauri/appdata
+1 -1
package.json
··· 2 2 "private": true, 3 3 "scripts": { 4 4 "tauri": "tauri", 5 - "dev": "tauri dev", 5 + "dev": "DEVELOPMENT=1 tauri dev", 6 6 "dev:web": "vite", 7 7 "build": "tauri build", 8 8 "build:web": "vite build",
+12
src-tauri/Cargo.lock
··· 91 91 ] 92 92 93 93 [[package]] 94 + name = "atomicwrites" 95 + version = "0.3.1" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "eb8f2cd6962fa53c0e2a9d3f97eaa7dbd1e3cbbeeb4745403515b42ae07b3ff6" 98 + dependencies = [ 99 + "tempfile", 100 + "winapi", 101 + ] 102 + 103 + [[package]] 94 104 name = "autocfg" 95 105 version = "1.1.0" 96 106 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2272 2282 version = "0.1.0" 2273 2283 dependencies = [ 2274 2284 "async-cron-scheduler", 2285 + "atomicwrites", 2275 2286 "chrono", 2276 2287 "cocoa", 2277 2288 "cron", ··· 2280 2291 "nanoid", 2281 2292 "objc", 2282 2293 "once_cell", 2294 + "rfd", 2283 2295 "serde", 2284 2296 "serde_json", 2285 2297 "tao",
+2
src-tauri/Cargo.toml
··· 26 26 tao = "0.12" 27 27 once_cell = "1.13" 28 28 cron = "0.11" 29 + rfd = "0.9" 30 + atomicwrites = "0.3" 29 31 # https://github.com/h4llow3En/mac-notification-sys/issues/43 30 32 mac-notification-sys = "=0.5.2" 31 33
+89
src-tauri/src/data.rs
··· 1 + use crate::notifications::Group; 2 + use atomicwrites::{AtomicFile, OverwriteBehavior}; 3 + use serde::{Deserialize, Serialize}; 4 + use serde_json::Value; 5 + use std::env; 6 + use std::io::Write; 7 + use std::path::PathBuf; 8 + use tauri::Config; 9 + 10 + #[derive(Clone)] 11 + pub struct AppPaths { 12 + pub app_dir: PathBuf, 13 + pub reminders_file: PathBuf, 14 + } 15 + impl AppPaths { 16 + pub fn from_tauri_config(config: &Config) -> Self { 17 + let app_dir = match env::var("DEVELOPMENT").is_ok() { 18 + true => env::current_dir().unwrap().join("appdata"), 19 + false => tauri::api::path::app_dir(config).unwrap(), 20 + }; 21 + AppPaths { 22 + app_dir: app_dir.clone(), 23 + reminders_file: app_dir.join("Settings.json"), 24 + } 25 + } 26 + } 27 + 28 + pub fn to_json<T: Serialize>(data: &T) -> Result<Value, String> { 29 + match serde_json::to_value(data) { 30 + Ok(v) => Ok(v), 31 + Err(e) => throw!("Error serializing {}", e), 32 + } 33 + } 34 + 35 + pub fn ensure_parent_exists(file_path: &PathBuf) -> Result<(), String> { 36 + if let Some(parent) = file_path.parent() { 37 + if let Err(e) = std::fs::create_dir_all(parent) { 38 + throw!("Error creating parent folder: {}", e.to_string()); 39 + } 40 + } 41 + Ok(()) 42 + } 43 + 44 + pub fn write_atomically(file_path: &PathBuf, buf: &[u8]) -> Result<(), String> { 45 + ensure_parent_exists(&file_path)?; 46 + let af = AtomicFile::new(&file_path, OverwriteBehavior::AllowOverwrite); 47 + match af.write(|f| f.write_all(&buf)) { 48 + Ok(_) => Ok(()), 49 + Err(e) => Err(e.to_string()), 50 + } 51 + } 52 + 53 + #[derive(Serialize, Deserialize, Clone)] 54 + pub struct RemindersFile { 55 + pub groups: Vec<Group>, 56 + } 57 + impl RemindersFile { 58 + pub fn load(paths: &AppPaths) -> Result<Self, String> { 59 + let reminders = match std::fs::read_to_string(&paths.reminders_file) { 60 + Ok(reminders_str) => { 61 + let reminders: RemindersFile = match serde_json::from_str(&reminders_str) { 62 + Ok(reminders) => reminders, 63 + Err(e) => throw!("Could not parse reminders file: {}", e), 64 + }; 65 + reminders 66 + } 67 + Err(e) => match e.kind() { 68 + std::io::ErrorKind::NotFound => RemindersFile { groups: Vec::new() }, 69 + _ => throw!("{}", e.to_string()), 70 + }, 71 + }; 72 + Ok(reminders) 73 + } 74 + 75 + pub fn save(&self, paths: &AppPaths) -> Result<(), String> { 76 + let mut json = Vec::new(); 77 + let formatter = serde_json::ser::PrettyFormatter::with_indent(b"\t"); 78 + let mut ser = serde_json::Serializer::with_formatter(&mut json, formatter); 79 + match self.serialize(&mut ser) { 80 + Ok(_) => {} 81 + Err(e) => throw!("Error saving content: {}", e.to_string()), 82 + } 83 + match write_atomically(&paths.reminders_file, &json) { 84 + Ok(_) => {} 85 + Err(e) => throw!("Error saving: {}", e.to_string()), 86 + } 87 + Ok(()) 88 + } 89 + }
+22 -12
src-tauri/src/main.rs
··· 7 7 use cocoa::appkit::NSApplicationActivationPolicy::{ 8 8 NSApplicationActivationPolicyAccessory, NSApplicationActivationPolicyRegular, 9 9 }; 10 - use notifications::{Data, Group, Instance}; 10 + use notifications::{Data, Instance}; 11 11 use std::sync::Mutex; 12 12 use std::thread; 13 13 use tauri::api::{dialog, shell}; ··· 32 32 }); 33 33 } 34 34 35 + fn error_popup_main_thread(msg: impl AsRef<str>) { 36 + let msg = msg.as_ref().to_string(); 37 + let builder = rfd::MessageDialog::new() 38 + .set_title("Error") 39 + .set_description(&msg) 40 + .set_buttons(rfd::MessageButtons::Ok) 41 + .set_level(rfd::MessageLevel::Info); 42 + builder.show(); 43 + } 44 + 45 + mod data; 35 46 mod notifications; 36 47 37 48 fn main() { ··· 42 53 #[cfg(target_os = "macos")] 43 54 macos_app_nap::prevent(); 44 55 45 - // let groups = Vec::new(); 46 - let groups = vec![Group { 47 - title: "Test".to_string(), 48 - description: "x".to_string(), 49 - enabled: true, 50 - id: "0".to_string(), 51 - job_id: None, 52 - cron: "*/10 * * * * *".to_string(), 53 - next_date: None, 54 - }]; 56 + let app_paths = data::AppPaths::from_tauri_config(ctx.config()); 57 + let reminders_file = match data::RemindersFile::load(&app_paths) { 58 + Ok(groups) => groups, 59 + Err(e) => { 60 + error_popup_main_thread(e); 61 + data::RemindersFile { groups: Vec::new() } 62 + } 63 + }; 55 64 let instance = Instance { 56 - groups, 65 + file: reminders_file, 66 + app_paths, 57 67 scheduler: None, 58 68 bundle_identifier: ctx.config().tauri.bundle.identifier.clone(), 59 69 };
+18 -19
src-tauri/src/notifications.rs
··· 1 + use crate::data::{to_json, AppPaths, RemindersFile}; 1 2 use async_cron_scheduler::{Job, JobId, Scheduler}; 2 3 use chrono::offset::Local; 3 4 use nanoid::nanoid; ··· 55 56 } 56 57 57 58 pub struct Instance { 58 - pub groups: Vec<Group>, 59 + pub file: RemindersFile, 59 60 pub scheduler: Option<Scheduler<Local>>, 61 + pub app_paths: AppPaths, 60 62 pub bundle_identifier: String, 61 63 } 62 64 impl Instance { 65 + pub fn save(&self) -> Result<(), String> { 66 + self.file.save(&self.app_paths) 67 + } 63 68 pub fn add_group(&mut self, mut group: Group) -> Result<(), String> { 64 69 match &mut self.scheduler { 65 70 Some(scheduler) => { 66 71 group.create_job(scheduler, self.bundle_identifier.clone())?; 67 - self.groups.push(group); 72 + self.file.groups.push(group); 68 73 } 69 74 None => { 70 - self.groups.push(group); 75 + self.file.groups.push(group); 71 76 self.start(); 72 77 } 73 78 }; ··· 80 85 ]; 81 86 for _ in 0..100 { 82 87 let id = nanoid!(7, &alphabet); 83 - let exists = self.groups.iter().any(|g| g.id == id); 88 + let exists = self.file.groups.iter().any(|g| g.id == id); 84 89 if !exists { 85 90 return id; 86 91 } ··· 91 96 let scheduler = match &mut self.scheduler { 92 97 Some(scheduler) => scheduler, 93 98 None => { 94 - self.groups.remove(index); 99 + self.file.groups.remove(index); 95 100 return; 96 101 } 97 102 }; 98 - match self.groups[index].job_id { 103 + match self.file.groups[index].job_id { 99 104 Some(job_id) => scheduler.remove(job_id), 100 105 None => {} 101 106 }; 102 - self.groups.remove(index); 107 + self.file.groups.remove(index); 103 108 } 104 109 pub fn start(&mut self) { 105 - let groups = self.groups.clone(); 106 110 let bundle_identifier = self.bundle_identifier.clone(); 107 111 108 112 let (mut scheduler, sched_service) = Scheduler::<Local>::launch(tokio::time::sleep); 109 113 110 114 let mut errors = Vec::new(); 111 - for mut group in groups { 115 + for group in &mut self.file.groups { 112 116 match group.create_job(&mut scheduler, bundle_identifier.clone()) { 113 117 Ok(_) => {} 114 118 Err(e) => errors.push(e), ··· 128 132 129 133 pub struct Data(pub Mutex<Instance>); 130 134 131 - pub fn to_json<T: Serialize>(data: &T) -> Result<Value, String> { 132 - match serde_json::to_value(data) { 133 - Ok(v) => Ok(v), 134 - Err(e) => throw!("Error serializing {}", e), 135 - } 136 - } 137 - 138 135 #[command] 139 136 pub async fn get_groups(data: State<'_, Data>) -> Result<Value, String> { 140 137 let data = data.0.lock().unwrap(); 141 - to_json(&data.groups) 138 + to_json(&data.file.groups) 142 139 } 143 140 144 141 #[command] ··· 146 143 let mut data = data.0.lock().unwrap(); 147 144 group.id = data.generate_id(); 148 145 data.add_group(group)?; 149 - to_json(&data.groups) 146 + data.save()?; 147 + to_json(&data.file.groups) 150 148 } 151 149 152 150 #[command] 153 151 pub async fn delete_group(index: usize, data: State<'_, Data>) -> Result<Value, String> { 154 152 let mut data = data.0.lock().unwrap(); 155 153 data.delete_group(index); 156 - to_json(&data.groups) 154 + data.save()?; 155 + to_json(&data.file.groups) 157 156 }