read vidko from config file
This commit is contained in:
parent
1b87769456
commit
770359d531
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1305,6 +1305,7 @@ dependencies = [
|
|||||||
"ical",
|
"ical",
|
||||||
"image",
|
"image",
|
||||||
"lazy-regex",
|
"lazy-regex",
|
||||||
|
"lazy_static",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"serde",
|
"serde",
|
||||||
"toml",
|
"toml",
|
||||||
|
@ -17,3 +17,4 @@ lazy-regex = "2.4.1"
|
|||||||
directories-next = "2.0.0"
|
directories-next = "2.0.0"
|
||||||
toml = "0.5.11"
|
toml = "0.5.11"
|
||||||
serde = { version = "1.0.152", features = ["derive"]}
|
serde = { version = "1.0.152", features = ["derive"]}
|
||||||
|
lazy_static = "1.4.0"
|
546
src/app.rs
546
src/app.rs
@ -1,45 +1,17 @@
|
|||||||
use std::ops::Add;
|
use std::{ops::Add, rc::Rc, cell::{Cell, RefCell}, f64::consts::E};
|
||||||
|
|
||||||
use eframe::{egui, CreationContext, epaint::text::TextWrapping};
|
use eframe::{egui, CreationContext};
|
||||||
use chrono::{Datelike, Timelike, Utc, NaiveDate, Weekday, IsoWeek, Duration, DateTime, NaiveDateTime, Days, Local};
|
use chrono::{Datelike, NaiveDate, Weekday, IsoWeek, Duration, Days, Local};
|
||||||
use egui::{Color32, ColorImage, TextureHandle, TextureOptions, Rect, text::LayoutJob, Visuals, Stroke};
|
use egui::{ColorImage, TextureOptions};
|
||||||
use crate::{timetable::{Timetable, get_timetable, Event, EventCategory}, config::ConfigStorage};
|
use crate::{timetable::{Timetable, Event, TimetableGetter, GetTimetableError}, config::{ConfigStore, Config}, events_table::EventsTable};
|
||||||
|
|
||||||
struct EventsTableStyle {
|
use crate::utils::load_image_from_memory;
|
||||||
highlight_color: Color32,
|
|
||||||
bg_fill: Color32,
|
|
||||||
fg_stroke: egui::Stroke
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
const MAX_FUTURE_WEEKS: u64 = 4 * 12;
|
||||||
let highlight_color = style.visuals.selection.bg_fill;
|
|
||||||
let bg_fill = style.visuals.widgets.noninteractive.bg_fill; //Color32::from_rgb(18, 18, 18);
|
|
||||||
let fg_stroke = style.visuals.widgets.active.fg_stroke;
|
|
||||||
|
|
||||||
let light_bg_col = highlight_color; // shift_color(bg_col, 1.5); // Color32::from_rgb(30, 30, 30);
|
lazy_static! {
|
||||||
let dark_bg_col = shift_color(bg_fill, 0.5); // style.visuals.widgets.noninteractive.bg_stroke.color; Color32::from_rgb(11, 8, 8);
|
pub static ref BREAK_IMAGE: ColorImage = load_image_from_memory(include_bytes!("../assets/break-area.png"))
|
||||||
//let fg_col = Color32::from_rgb(252, 232, 195);
|
.expect("Failed to decode break area texture") as ColorImage;
|
||||||
let now_line_col = shift_color(highlight_color, 1.5); // fg_stroke.color; // shift_color(highlight_color, 1.5); // Color32::from_rgb(44, 120, 191);
|
|
||||||
*/
|
|
||||||
impl EventsTableStyle {
|
|
||||||
fn from_visuals(visuals: &Visuals) -> Self {
|
|
||||||
let bg_fill = visuals.widgets.noninteractive.bg_fill;
|
|
||||||
Self {
|
|
||||||
highlight_color: visuals.selection.bg_fill,
|
|
||||||
bg_fill,
|
|
||||||
fg_stroke: visuals.widgets.active.fg_stroke,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn dark_bg_fill(&self) -> Color32 {
|
|
||||||
shift_color(self.bg_fill, 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn now_line_fill(&self) -> Color32 {
|
|
||||||
shift_color(self.highlight_color, 1.5)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppAssets {
|
struct AppAssets {
|
||||||
@ -47,131 +19,19 @@ struct AppAssets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct MainApp {
|
pub struct MainApp {
|
||||||
pub vidko: Option<String>,
|
|
||||||
pub timetable: Option<Timetable>,
|
|
||||||
shown_week: IsoWeek,
|
shown_week: IsoWeek,
|
||||||
shown_events: Vec<Event>,
|
shown_events: Vec<Event>,
|
||||||
|
|
||||||
|
timetable_getter: Box<dyn TimetableGetter>,
|
||||||
|
pub timetable: Option<Timetable>,
|
||||||
|
|
||||||
|
config_store: Box<dyn ConfigStore>,
|
||||||
|
config: Option<Config>,
|
||||||
|
|
||||||
assets: Option<AppAssets>,
|
assets: Option<AppAssets>,
|
||||||
storage: ConfigStorage
|
vidko_textfield: String,
|
||||||
}
|
|
||||||
|
|
||||||
fn count_minutes(time: &str) -> u32 {
|
screen: Option<Rc<RefCell<dyn Screen>>>
|
||||||
let (time_h, time_m) = time.split_once(":").unwrap();
|
|
||||||
return 60*time_h.parse::<u32>().unwrap() + time_m.parse::<u32>().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_category_bg(category: EventCategory) -> Color32 {
|
|
||||||
match category {
|
|
||||||
EventCategory::Default => Color32::GRAY,
|
|
||||||
EventCategory::Yellow => Color32::from_rgb(251, 184, 41),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_bright_color(color: Color32) -> bool {
|
|
||||||
return color.r() + color.g() + color.b() > 128 * 3
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
|
|
||||||
let image = image::load_from_memory(image_data)?;
|
|
||||||
let size = [image.width() as _, image.height() as _];
|
|
||||||
let image_buffer = image.to_rgba8();
|
|
||||||
let pixels = image_buffer.as_flat_samples();
|
|
||||||
Ok(ColorImage::from_rgba_unmultiplied(
|
|
||||||
size,
|
|
||||||
pixels.as_slice(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_repeating_texture(ui: &mut egui::Ui, texture: &TextureHandle, rect: Rect, tint: Color32) {
|
|
||||||
use egui::*;
|
|
||||||
let painter = ui.painter();
|
|
||||||
|
|
||||||
let texture_size = texture.size_vec2();
|
|
||||||
let horizontal_count = rect.width()/texture_size[0];
|
|
||||||
let vertical_count = rect.height()/texture_size[1];
|
|
||||||
|
|
||||||
let draw_tile = |ix: u32, iy: u32, scale: Vec2| {
|
|
||||||
painter.image(
|
|
||||||
texture.id(),
|
|
||||||
Rect::from_min_size(
|
|
||||||
rect.left_top() + texture_size * vec2(ix as f32, iy as f32),
|
|
||||||
texture_size * scale
|
|
||||||
),
|
|
||||||
Rect::from_min_max(pos2(0.0, 0.0), scale.to_pos2()),
|
|
||||||
tint
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let scale = vec2(1.0, 1.0);
|
|
||||||
for ix in 0..horizontal_count.floor() as u32 {
|
|
||||||
for iy in 0..vertical_count.floor() as u32 {
|
|
||||||
draw_tile(ix, iy, scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// right edge
|
|
||||||
{
|
|
||||||
let scale = vec2(horizontal_count % 1.0, 1.0);
|
|
||||||
let ix = horizontal_count.floor() as u32;
|
|
||||||
for iy in 0..vertical_count.floor() as u32 {
|
|
||||||
draw_tile(ix, iy, scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// bottom edge
|
|
||||||
{
|
|
||||||
let scale = vec2(1.0, vertical_count % 1.0);
|
|
||||||
let iy = vertical_count.floor() as u32;
|
|
||||||
for ix in 0..horizontal_count.floor() as u32 {
|
|
||||||
draw_tile(ix, iy, scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// left bottom corner
|
|
||||||
{
|
|
||||||
let scale = vec2(horizontal_count % 1.0, vertical_count % 1.0);
|
|
||||||
draw_tile(horizontal_count.floor() as u32, vertical_count.floor() as u32, scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_event_card(ui: &mut egui::Ui, event: &Event, mut rect: Rect) {
|
|
||||||
use egui::*;
|
|
||||||
let margin = 6.0;
|
|
||||||
let border_size = 4.0;
|
|
||||||
let text_size = egui::TextStyle::Body.resolve(ui.style()).size;
|
|
||||||
let text_color = Color32::BLACK;
|
|
||||||
|
|
||||||
rect.set_width(rect.width().max(text_size*6.0));
|
|
||||||
|
|
||||||
let painter = ui.painter();
|
|
||||||
let color = get_category_bg(event.category);
|
|
||||||
let rounding = Rounding::from(5.0);
|
|
||||||
let border_color = color.linear_multiply(1.25);
|
|
||||||
painter.rect_filled(rect, rounding, color);
|
|
||||||
painter.rect_stroke(rect.shrink(border_size/2.0), rounding, (border_size, border_color));
|
|
||||||
|
|
||||||
ui.allocate_ui_at_rect(rect.shrink(margin), |ui| {
|
|
||||||
let font = FontId::proportional(text_size * 0.8);
|
|
||||||
let summary_format = TextFormat {
|
|
||||||
color: text_color,
|
|
||||||
font_id: font.clone(),
|
|
||||||
..TextFormat::default()
|
|
||||||
};
|
|
||||||
let module_name = event.module_name.as_ref().unwrap_or(&event.summary);
|
|
||||||
let mut job = LayoutJob::single_section(module_name.to_string(), summary_format);
|
|
||||||
job.wrap = TextWrapping {
|
|
||||||
max_rows: 2,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
ui.label(job);
|
|
||||||
|
|
||||||
ui.add_space(text_size*0.2);
|
|
||||||
let time_label = format!("{} - {}", event.start_time.format("%H:%M"), event.end_time.format("%H:%M"));
|
|
||||||
ui.label(RichText::new(time_label).color(text_color).font(font));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@ -180,15 +40,6 @@ fn is_weekend(time: NaiveDate) -> bool {
|
|||||||
return day == Weekday::Sat || day == Weekday::Sun;
|
return day == Weekday::Sat || day == Weekday::Sun;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn shift_color(color: Color32, amount: f32) -> Color32 {
|
|
||||||
return Color32::from_rgb(
|
|
||||||
(color.r() as f32 * amount) as u8,
|
|
||||||
(color.g() as f32 * amount) as u8,
|
|
||||||
(color.b() as f32 * amount) as u8
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_week() -> IsoWeek {
|
fn get_current_week() -> IsoWeek {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
if is_weekend(now.date_naive()) {
|
if is_weekend(now.date_naive()) {
|
||||||
@ -198,212 +49,169 @@ fn get_current_week() -> IsoWeek {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
fn get_future_week(week_offset: u64) -> IsoWeek {
|
||||||
fn show_events_table_header(
|
let now_week = get_current_week();
|
||||||
ui: &mut egui::Ui,
|
let year = now_week.year();
|
||||||
style: &EventsTableStyle,
|
let week = now_week.week();
|
||||||
rect: Rect,
|
let week_date = NaiveDate::from_isoywd_opt(now_week.year(), now_week.week(), Weekday::Mon).expect("Invalid week or year given");
|
||||||
week: IsoWeek
|
week_date.checked_add_days(Days::new(7 * week_offset)).unwrap().iso_week()
|
||||||
) {
|
}
|
||||||
use egui::*;
|
|
||||||
let painter = ui.painter();
|
|
||||||
painter.rect_filled(rect, Rounding::none(), style.dark_bg_fill());
|
|
||||||
|
|
||||||
let column_width = rect.width()/5.0;
|
trait Screen {
|
||||||
let header_size = rect.height();
|
fn show(&mut self, app: &mut MainApp, ctx: &egui::Context);
|
||||||
let text_size = egui::TextStyle::Body.resolve(ui.style()).size;
|
}
|
||||||
|
|
||||||
// Draw day names
|
#[derive(Default)]
|
||||||
for (i, name) in ["Pir", "Ant", "Tre", "Ket", "Pen"].iter().enumerate() {
|
struct MainScreen {}
|
||||||
let offset = column_width * (i as f32 + 0.5);
|
impl Screen for MainScreen {
|
||||||
|
fn show(&mut self, app: &mut MainApp, ctx: &egui::Context) {
|
||||||
|
use egui::*;
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(Frame::none())
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
if ctx.input().key_pressed(Key::D) && app.shown_week < get_future_week(MAX_FUTURE_WEEKS) {
|
||||||
|
app.shift_shown_week(1);
|
||||||
|
}
|
||||||
|
if ctx.input().key_pressed(Key::A) && get_current_week() < app.shown_week {
|
||||||
|
app.shift_shown_week(-1);
|
||||||
|
}
|
||||||
|
if ctx.input().key_pressed(Key::S) {
|
||||||
|
app.set_shown_week(get_current_week());
|
||||||
|
}
|
||||||
|
if ctx.input().key_pressed(Key::F2) {
|
||||||
|
if ctx.style().visuals.dark_mode {
|
||||||
|
ctx.set_visuals(egui::Visuals::light());
|
||||||
|
} else {
|
||||||
|
ctx.set_visuals(egui::Visuals::dark());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
painter.text(
|
let mut events_table = EventsTable::new(&app.shown_events);
|
||||||
rect.left_top() + vec2(offset, header_size/2.5),
|
events_table.week = Some(app.shown_week);
|
||||||
Align2::CENTER_CENTER,
|
events_table.now = Some(Local::now().naive_local());
|
||||||
name,
|
events_table.break_texture = Some(app.assets.as_ref().unwrap().break_texture.clone());
|
||||||
FontId::monospace(text_size*1.2),
|
ui.add(events_table);
|
||||||
style.fg_stroke.color
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw dates
|
|
||||||
let year = week.year();
|
|
||||||
let week = week.week();
|
|
||||||
let mut week_date = NaiveDate::from_isoywd_opt(year, week, Weekday::Mon).expect("Invalid week or year given");
|
|
||||||
for i in 1..=5 {
|
|
||||||
let offset = column_width * i as f32;
|
|
||||||
|
|
||||||
painter.text(
|
|
||||||
rect.left_top() + vec2(offset-3.0, header_size-3.0),
|
|
||||||
Align2::RIGHT_BOTTOM,
|
|
||||||
week_date.format("%m-%d").to_string(),
|
|
||||||
FontId::proportional(text_size*0.85),
|
|
||||||
style.fg_stroke.color
|
|
||||||
);
|
|
||||||
week_date = week_date.add(Duration::days(1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[derive(Default)]
|
||||||
fn show_events_table_body(
|
struct VidkoScreen {
|
||||||
ui: &mut egui::Ui,
|
vidko_textfield: String,
|
||||||
style: &EventsTableStyle,
|
get_error: Option<GetTimetableError>
|
||||||
assets: &AppAssets,
|
|
||||||
rect: Rect,
|
|
||||||
week: IsoWeek,
|
|
||||||
now: NaiveDateTime,
|
|
||||||
events: &[Event]
|
|
||||||
) {
|
|
||||||
use egui::*;
|
|
||||||
|
|
||||||
let painter = ui.painter();
|
|
||||||
let column_width = rect.width()/5.0;
|
|
||||||
let column_gap = 3.0;
|
|
||||||
|
|
||||||
let timestamps = ["9:00", "10:30", "11:00", "12:30", "13:30", "15:00", "15:30", "17:00"];
|
|
||||||
let timestamps_mins = timestamps.map(count_minutes);
|
|
||||||
let total_minutes = timestamps_mins.last().unwrap() - timestamps_mins.first().unwrap();
|
|
||||||
let minute_to_pixel_scale = rect.height()/total_minutes as f32;
|
|
||||||
|
|
||||||
// draw bg
|
|
||||||
painter.rect_filled(
|
|
||||||
rect,
|
|
||||||
Rounding::none(),
|
|
||||||
style.bg_fill
|
|
||||||
);
|
|
||||||
|
|
||||||
// Highlight current day column
|
|
||||||
if now.iso_week() == week && !is_weekend(now.date()) {
|
|
||||||
let days_from_monday = now.weekday().num_days_from_monday() as f32;
|
|
||||||
let rect = Rect::from_min_max(
|
|
||||||
rect.left_top() + vec2(column_width * days_from_monday, 0.0),
|
|
||||||
rect.left_bottom() + vec2(column_width * (1.0+days_from_monday), 0.0)
|
|
||||||
);
|
|
||||||
painter.rect_filled(rect, Rounding::none(), style.highlight_color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw gaps between columns
|
|
||||||
for i in 1..5 {
|
|
||||||
let offset = column_width * i as f32;
|
|
||||||
painter.line_segment([
|
|
||||||
rect.left_top() + vec2(offset, 0.0),
|
|
||||||
rect.left_bottom() + vec2(offset, 0.0)
|
|
||||||
], (column_gap, style.dark_bg_fill()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark break times
|
|
||||||
for i in (1..timestamps_mins.len()-1).step_by(2) {
|
|
||||||
let from = (timestamps_mins[i] - timestamps_mins[0]) as f32 * minute_to_pixel_scale;
|
|
||||||
let to = (timestamps_mins[i+1] - timestamps_mins[0]) as f32 * minute_to_pixel_scale;
|
|
||||||
draw_repeating_texture(
|
|
||||||
ui,
|
|
||||||
&assets.break_texture,
|
|
||||||
Rect::from_min_size(
|
|
||||||
rect.left_top() + vec2(0.0, from),
|
|
||||||
vec2(rect.width(), to - from)
|
|
||||||
),
|
|
||||||
style.dark_bg_fill()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw event cards
|
|
||||||
for event in events {
|
|
||||||
let day = event.date.weekday().num_days_from_monday() as usize;
|
|
||||||
let duration = (event.end_time - event.start_time).num_minutes() as f32;
|
|
||||||
let start_time = event.start_time.hour()*60 + event.start_time.minute() - timestamps_mins[0];
|
|
||||||
let event_rect = Rect::from_min_size(
|
|
||||||
rect.left_top() + vec2(column_width*day as f32, start_time as f32*minute_to_pixel_scale),
|
|
||||||
vec2(column_width, duration*minute_to_pixel_scale)
|
|
||||||
).shrink2(vec2(10.0, 0.0));
|
|
||||||
show_event_card(ui, event, event_rect);
|
|
||||||
}
|
|
||||||
|
|
||||||
// now line
|
|
||||||
let painter = ui.painter();
|
|
||||||
let current_time = now.minute() + now.hour() * 60 - timestamps_mins[0];
|
|
||||||
if current_time > 0 && current_time < *timestamps_mins.last().unwrap_or(&0) && !is_weekend(now.date()) {
|
|
||||||
let offset = current_time as f32 * minute_to_pixel_scale;
|
|
||||||
let points = [
|
|
||||||
rect.left_top() + vec2(0.0, offset),
|
|
||||||
rect.right_top() + vec2(0.0, offset)
|
|
||||||
];
|
|
||||||
let thickness = 2.0;
|
|
||||||
let border_size = 2.0;
|
|
||||||
painter.line_segment(points, (thickness + 2.0 * border_size, style.dark_bg_fill()));
|
|
||||||
painter.line_segment(points, (thickness, style.highlight_color));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
impl Screen for VidkoScreen {
|
||||||
|
fn show(&mut self, app: &mut MainApp, ctx: &egui::Context) {
|
||||||
|
use egui::*;
|
||||||
|
|
||||||
fn show_events_table(
|
egui::CentralPanel::default()
|
||||||
assets: &AppAssets,
|
.show(ctx, |ui| {
|
||||||
ui: &mut egui::Ui,
|
ui.vertical(|ui| {
|
||||||
rect: Rect,
|
ui.label("Įveskite savo vidko kodą");
|
||||||
shown_week: IsoWeek,
|
ui.horizontal(|ui| {
|
||||||
now: NaiveDateTime,
|
ui.label("Vidko: ");
|
||||||
events: &[Event]
|
ui.text_edit_singleline(&mut self.vidko_textfield);
|
||||||
) {
|
});
|
||||||
use egui::*;
|
if ui.button("Įvesti").clicked() {
|
||||||
|
match app.timetable_getter.get(&self.vidko_textfield) {
|
||||||
let header_size = 50.0;
|
Ok(timetable) => {
|
||||||
|
if app.config.is_none() {
|
||||||
//let now = now.checked_add_days(Days::new(1)).unwrap();
|
app.config = Some(Config::default());
|
||||||
let style = EventsTableStyle::from_visuals(&ui.style().visuals);
|
}
|
||||||
|
app.config.as_mut().unwrap().vidko = Some(self.vidko_textfield.clone());
|
||||||
show_events_table_header(
|
app.set_timetable(timetable);
|
||||||
ui, &style,
|
app.switch_to_main();
|
||||||
Rect::from_min_size(
|
},
|
||||||
rect.left_top(),
|
Err(e) => {
|
||||||
vec2(rect.width(), header_size)
|
self.get_error = Some(e);
|
||||||
),
|
},
|
||||||
shown_week
|
}
|
||||||
);
|
}
|
||||||
|
if self.get_error.is_some() {
|
||||||
show_events_table_body(
|
ui.colored_label(Color32::RED, "Netinkamas kodas");
|
||||||
ui, &style, assets,
|
}
|
||||||
Rect::from_min_max(
|
});
|
||||||
rect.left_top() + vec2(0.0, header_size),
|
});
|
||||||
rect.right_bottom()
|
}
|
||||||
),
|
|
||||||
shown_week,
|
|
||||||
now,
|
|
||||||
events
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainApp {
|
impl MainApp {
|
||||||
pub fn new(storage: ConfigStorage) -> MainApp {
|
pub fn new(config_store: Box<dyn ConfigStore>, timetable_getter: Box<dyn TimetableGetter>) -> Self {
|
||||||
MainApp {
|
Self {
|
||||||
vidko: None,
|
|
||||||
timetable: None,
|
timetable: None,
|
||||||
shown_week: get_current_week(),
|
shown_week: get_current_week(),
|
||||||
shown_events: vec![],
|
shown_events: vec![],
|
||||||
assets: None,
|
assets: None,
|
||||||
storage
|
config_store,
|
||||||
|
config: None,
|
||||||
|
vidko_textfield: String::new(),
|
||||||
|
timetable_getter,
|
||||||
|
screen: None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_creation(&mut self, cc: &CreationContext) {
|
pub fn init(&mut self, cc: &CreationContext) {
|
||||||
self.storage.attempt_load();
|
|
||||||
self.vidko = self.storage.config.vidko_code.clone();
|
|
||||||
|
|
||||||
let break_image = load_image_from_memory(include_bytes!("../assets/break-area.png")).expect("Failed to decode break area texture");
|
let texture_handle = cc.egui_ctx.load_texture("break-area", BREAK_IMAGE.clone(), TextureOptions::LINEAR);
|
||||||
let texture_handle = cc.egui_ctx.load_texture("break-area", break_image, TextureOptions::LINEAR);
|
|
||||||
self.assets = Some(AppAssets {
|
self.assets = Some(AppAssets {
|
||||||
break_texture: texture_handle
|
break_texture: texture_handle
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.config = match self.config_store.load() {
|
||||||
|
Ok(config) => Some(config),
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.vidko().is_none() {
|
||||||
|
self.switch_to_vidko();
|
||||||
|
} else {
|
||||||
|
if self.refresh_timetable().is_err() {
|
||||||
|
self.switch_to_vidko();
|
||||||
|
} else {
|
||||||
|
self.switch_to_main();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_timetable(&mut self) {
|
fn switch_to_main(&mut self) {
|
||||||
if self.vidko.is_none() {
|
self.screen = Some(Rc::new(RefCell::new(MainScreen::default())))
|
||||||
self.shown_events = vec![];
|
}
|
||||||
self.timetable = None;
|
|
||||||
return;
|
fn switch_to_vidko(&mut self) {
|
||||||
|
self.screen = Some(Rc::new(RefCell::new(VidkoScreen::default())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn vidko(&self) -> Option<&str> {
|
||||||
|
if let Some(config) = &self.config {
|
||||||
|
return config.vidko.as_deref();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_timetable(&mut self) -> Result<(), GetTimetableError> {
|
||||||
|
let vidko;
|
||||||
|
{
|
||||||
|
if self.vidko().is_none() {
|
||||||
|
self.shown_events = vec![];
|
||||||
|
self.timetable = None;
|
||||||
|
return Err(GetTimetableError::NotFound);
|
||||||
|
}
|
||||||
|
vidko = self.vidko().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let timetable = get_timetable(self.vidko.as_ref().unwrap());
|
let timetable = match self.timetable_getter.get(vidko) {
|
||||||
if timetable.is_err() { return; }
|
Ok(timetable) => timetable,
|
||||||
let timetable = timetable.unwrap();
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.shown_events = timetable.by_week(self.shown_week);
|
||||||
|
self.timetable = Some(timetable);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_timetable(&mut self, timetable: Timetable) {
|
||||||
self.shown_events = timetable.by_week(self.shown_week);
|
self.shown_events = timetable.by_week(self.shown_week);
|
||||||
self.timetable = Some(timetable);
|
self.timetable = Some(timetable);
|
||||||
}
|
}
|
||||||
@ -435,42 +243,18 @@ impl MainApp {
|
|||||||
|
|
||||||
impl eframe::App for MainApp {
|
impl eframe::App for MainApp {
|
||||||
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
|
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
|
||||||
self.storage.attempt_save();
|
if let Some(config) = &self.config {
|
||||||
|
self.config_store.save(config).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
use egui::*;
|
|
||||||
|
|
||||||
//ctx.set_visuals(egui::Visuals::light());
|
if let Some(screen) = self.screen.clone() {
|
||||||
egui::CentralPanel::default()
|
screen.borrow_mut().show(self, ctx)
|
||||||
.frame(Frame::none())
|
} else {
|
||||||
.show(ctx, |ui| {
|
// TODO: show error screen
|
||||||
if ctx.input().key_pressed(Key::D) {
|
todo!()
|
||||||
self.shift_shown_week(1);
|
}
|
||||||
}
|
|
||||||
if ctx.input().key_pressed(Key::A) {
|
|
||||||
self.shift_shown_week(-1);
|
|
||||||
}
|
|
||||||
if ctx.input().key_pressed(Key::S) {
|
|
||||||
self.set_shown_week(get_current_week());
|
|
||||||
}
|
|
||||||
if ctx.input().key_pressed(Key::F2) {
|
|
||||||
if ctx.style().visuals.dark_mode {
|
|
||||||
ctx.set_visuals(egui::Visuals::light());
|
|
||||||
} else {
|
|
||||||
ctx.set_visuals(egui::Visuals::dark());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rect = ui.allocate_rect(ui.min_rect(), Sense::hover()).rect;
|
|
||||||
show_events_table(
|
|
||||||
self.assets.as_ref().unwrap(),
|
|
||||||
ui,
|
|
||||||
rect,
|
|
||||||
self.shown_week,
|
|
||||||
Local::now().naive_local(),
|
|
||||||
&self.shown_events
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
131
src/config.rs
131
src/config.rs
@ -1,64 +1,115 @@
|
|||||||
use std::{path::{Path, PathBuf}, fs};
|
use std::{path::{Path, PathBuf}, fs, io, error::Error, fmt};
|
||||||
|
|
||||||
use directories_next::ProjectDirs;
|
use directories_next::ProjectDirs;
|
||||||
use eframe::Storage;
|
|
||||||
use egui::util::cache::CacheStorage;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub vidko_code: Option<String>
|
pub vidko: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self { vidko: None }
|
||||||
vidko_code: Some("E0000".into())
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LoadConfigError {
|
||||||
|
NotFound,
|
||||||
|
FileError(io::Error),
|
||||||
|
TomlError(toml::de::Error)
|
||||||
|
}
|
||||||
|
impl Error for LoadConfigError {}
|
||||||
|
impl fmt::Display for LoadConfigError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
LoadConfigError::FileError(e) => write!(f, "File error: {}", e),
|
||||||
|
LoadConfigError::TomlError(e) => write!(f, "Toml error: {}", e),
|
||||||
|
LoadConfigError::NotFound => write!(f, "Not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ConfigStorage {
|
#[derive(Debug)]
|
||||||
pub config: Config,
|
pub enum SaveConfigError {
|
||||||
|
FileError(io::Error),
|
||||||
config_file: Option<PathBuf>
|
TomlError(toml::ser::Error)
|
||||||
|
}
|
||||||
|
impl Error for SaveConfigError {}
|
||||||
|
impl fmt::Display for SaveConfigError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
SaveConfigError::FileError(e) => write!(f, "File error: {}", e),
|
||||||
|
SaveConfigError::TomlError(e) => write!(f, "Toml error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConfigStorage {
|
pub trait ConfigStore {
|
||||||
|
fn load(&self) -> Result<Config, LoadConfigError>;
|
||||||
|
fn save(&self, config: &Config) -> Result<(), SaveConfigError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub struct TomlConfigStore {
|
||||||
|
filename: PathBuf
|
||||||
|
}
|
||||||
|
impl TomlConfigStore {
|
||||||
|
fn new(filename: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
filename: filename.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for TomlConfigStore {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let project_dirs = ProjectDirs::from("", "", "KTU Timetable").expect("Failed to determine project directories");
|
let project_dirs = ProjectDirs::from("", "", "KTU Timetable").expect("Failed to determine home directory");
|
||||||
let config_dir = project_dirs.config_dir();
|
let config_dir = project_dirs.config_dir();
|
||||||
Self {
|
Self::new(&config_dir.join("config.toml"))
|
||||||
config: Config::default(),
|
}
|
||||||
config_file: Some(config_dir.join("config.toml"))
|
}
|
||||||
|
impl ConfigStore for TomlConfigStore {
|
||||||
|
fn load(&self) -> Result<Config, LoadConfigError> {
|
||||||
|
let config_str = fs::read_to_string(&self.filename)
|
||||||
|
.map_err(|e| LoadConfigError::FileError(e))?;
|
||||||
|
|
||||||
|
toml::from_str(&config_str)
|
||||||
|
.map_err(|e| LoadConfigError::TomlError(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self, config: &Config) -> Result<(), SaveConfigError> {
|
||||||
|
let directory = Path::parent(&self.filename).unwrap();
|
||||||
|
if !Path::is_dir(directory) {
|
||||||
|
fs::create_dir_all(directory)
|
||||||
|
.map_err(|e| SaveConfigError::FileError(e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let config_str = toml::to_string_pretty(config)
|
||||||
|
.map_err(|e| SaveConfigError::TomlError(e))?;
|
||||||
|
|
||||||
|
fs::write(&self.filename, config_str)
|
||||||
|
.map_err(|e| SaveConfigError::FileError(e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigStorage {
|
|
||||||
pub fn memory() -> Self {
|
|
||||||
let mut config = Self::default();
|
|
||||||
config.config_file = None;
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attempt_load(&mut self) {
|
pub struct MemoryConfigStore {
|
||||||
if self.config_file.is_none() { return; }
|
config: Option<Config>
|
||||||
let config_file = self.config_file.as_ref().unwrap();
|
}
|
||||||
let config_str = fs::read_to_string(config_file);
|
impl MemoryConfigStore {
|
||||||
if let Err(_) = config_str {
|
pub fn new(config: Config) -> Self {
|
||||||
fs::write(config_file, toml::to_string_pretty(&Config::default()).unwrap()).unwrap();
|
Self { config: Some(config) }
|
||||||
}
|
|
||||||
let config_str = config_str.unwrap();
|
|
||||||
self.config = toml::from_str(&config_str).unwrap_or_default();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attempt_save(&self) {
|
|
||||||
if self.config_file.is_none() { return; }
|
|
||||||
let config_file = self.config_file.as_ref().unwrap();
|
|
||||||
let config_str = toml::to_string_pretty(&self.config).unwrap();
|
|
||||||
fs::write(config_file, config_str).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl ConfigStore for MemoryConfigStore {
|
||||||
|
fn load(&self) -> Result<Config, LoadConfigError> {
|
||||||
|
self.config.clone().ok_or(LoadConfigError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self, _config: &Config) -> Result<(), SaveConfigError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
338
src/events_table.rs
Normal file
338
src/events_table.rs
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
use std::ops::Add;
|
||||||
|
|
||||||
|
use eframe::{egui, epaint::text::TextWrapping};
|
||||||
|
use chrono::{Datelike, Timelike, NaiveDate, Weekday, IsoWeek, Duration, NaiveDateTime, Local};
|
||||||
|
use egui::{Color32, TextureHandle, Rect, text::LayoutJob, Visuals, Stroke, Widget};
|
||||||
|
use crate::timetable::{Event, EventCategory};
|
||||||
|
|
||||||
|
pub struct EventsTable<'a> {
|
||||||
|
pub break_texture: Option<TextureHandle>,
|
||||||
|
|
||||||
|
pub week: Option<IsoWeek>,
|
||||||
|
pub now: Option<NaiveDateTime>,
|
||||||
|
pub events: &'a [Event]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_minutes(time: &str) -> u32 {
|
||||||
|
let (time_h, time_m) = time.split_once(":").unwrap();
|
||||||
|
return 60*time_h.parse::<u32>().unwrap() + time_m.parse::<u32>().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_category_bg(category: EventCategory) -> Color32 {
|
||||||
|
match category {
|
||||||
|
EventCategory::Default => Color32::GRAY,
|
||||||
|
EventCategory::Yellow => Color32::from_rgb(251, 184, 41),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_repeating_texture(ui: &mut egui::Ui, texture: Option<&TextureHandle>, rect: Rect, tint: Color32) {
|
||||||
|
use egui::*;
|
||||||
|
let painter = ui.painter();
|
||||||
|
|
||||||
|
if texture.is_none() {
|
||||||
|
painter.rect(rect, Rounding::none(), tint, (1.0, tint));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let texture = texture.unwrap();
|
||||||
|
let texture_size = texture.size_vec2();
|
||||||
|
let horizontal_count = rect.width()/texture_size[0];
|
||||||
|
let vertical_count = rect.height()/texture_size[1];
|
||||||
|
|
||||||
|
let draw_tile = |ix: u32, iy: u32, scale: Vec2| {
|
||||||
|
painter.image(
|
||||||
|
texture.id(),
|
||||||
|
Rect::from_min_size(
|
||||||
|
rect.left_top() + texture_size * vec2(ix as f32, iy as f32),
|
||||||
|
texture_size * scale
|
||||||
|
),
|
||||||
|
Rect::from_min_max(pos2(0.0, 0.0), scale.to_pos2()),
|
||||||
|
tint
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let scale = vec2(1.0, 1.0);
|
||||||
|
for ix in 0..horizontal_count.floor() as u32 {
|
||||||
|
for iy in 0..vertical_count.floor() as u32 {
|
||||||
|
draw_tile(ix, iy, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// right edge
|
||||||
|
{
|
||||||
|
let scale = vec2(horizontal_count % 1.0, 1.0);
|
||||||
|
let ix = horizontal_count.floor() as u32;
|
||||||
|
for iy in 0..vertical_count.floor() as u32 {
|
||||||
|
draw_tile(ix, iy, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom edge
|
||||||
|
{
|
||||||
|
let scale = vec2(1.0, vertical_count % 1.0);
|
||||||
|
let iy = vertical_count.floor() as u32;
|
||||||
|
for ix in 0..horizontal_count.floor() as u32 {
|
||||||
|
draw_tile(ix, iy, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// left bottom corner
|
||||||
|
{
|
||||||
|
let scale = vec2(horizontal_count % 1.0, vertical_count % 1.0);
|
||||||
|
draw_tile(horizontal_count.floor() as u32, vertical_count.floor() as u32, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_weekend(time: NaiveDate) -> bool {
|
||||||
|
let day = time.weekday();
|
||||||
|
return day == Weekday::Sat || day == Weekday::Sun;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn shift_color(color: Color32, amount: f32) -> Color32 {
|
||||||
|
return Color32::from_rgb(
|
||||||
|
(color.r() as f32 * amount) as u8,
|
||||||
|
(color.g() as f32 * amount) as u8,
|
||||||
|
(color.b() as f32 * amount) as u8
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEADER_SIZE: f32 = 50.0;
|
||||||
|
|
||||||
|
impl<'a> EventsTable<'a> {
|
||||||
|
pub fn new(events: &'a [Event]) -> Self {
|
||||||
|
Self {
|
||||||
|
break_texture: None,
|
||||||
|
week: None,
|
||||||
|
now: None,
|
||||||
|
events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn fg_stroke(&self, visuals: &Visuals) -> Stroke {
|
||||||
|
visuals.widgets.active.fg_stroke
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn bg_fill(&self, visuals: &Visuals) -> Color32 {
|
||||||
|
visuals.widgets.noninteractive.bg_fill
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn highlight_color(&self, visuals: &Visuals) -> Color32 {
|
||||||
|
visuals.selection.bg_fill
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn dark_bg_fill(&self, visuals: &Visuals) -> Color32 {
|
||||||
|
shift_color(self.bg_fill(visuals), 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_event(&self, ui: &mut egui::Ui, event: &Event, mut rect: Rect) {
|
||||||
|
use egui::*;
|
||||||
|
let margin = 6.0;
|
||||||
|
let border_size = 4.0;
|
||||||
|
let text_size = egui::TextStyle::Body.resolve(ui.style()).size;
|
||||||
|
let text_color = Color32::BLACK;
|
||||||
|
|
||||||
|
rect.set_width(rect.width().max(text_size*6.0));
|
||||||
|
|
||||||
|
let painter = ui.painter();
|
||||||
|
let color = get_category_bg(event.category);
|
||||||
|
let rounding = Rounding::from(5.0);
|
||||||
|
let border_color = color.linear_multiply(1.25);
|
||||||
|
painter.rect_filled(rect, rounding, color);
|
||||||
|
painter.rect_stroke(rect.shrink(border_size/2.0), rounding, (border_size, border_color));
|
||||||
|
|
||||||
|
ui.allocate_ui_at_rect(rect.shrink(margin), |ui| {
|
||||||
|
let font = FontId::proportional(text_size * 0.8);
|
||||||
|
let summary_format = TextFormat {
|
||||||
|
color: text_color,
|
||||||
|
font_id: font.clone(),
|
||||||
|
..TextFormat::default()
|
||||||
|
};
|
||||||
|
let module_name = event.module_name.as_ref().unwrap_or(&event.summary);
|
||||||
|
let mut job = LayoutJob::single_section(module_name.to_string(), summary_format);
|
||||||
|
job.wrap = TextWrapping {
|
||||||
|
max_rows: 2,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
ui.label(job);
|
||||||
|
|
||||||
|
ui.add_space(text_size*0.2);
|
||||||
|
let time_label = format!("{} - {}", event.start_time.format("%H:%M"), event.end_time.format("%H:%M"));
|
||||||
|
ui.label(RichText::new(time_label).color(text_color).font(font));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_header(&self, ui: &mut egui::Ui, rect: Rect, week: IsoWeek) {
|
||||||
|
use egui::*;
|
||||||
|
let painter = ui.painter();
|
||||||
|
let visuals = ui.visuals();
|
||||||
|
painter.rect_filled(rect, Rounding::none(), self.dark_bg_fill(visuals));
|
||||||
|
|
||||||
|
let column_width = rect.width()/5.0;
|
||||||
|
let header_size = rect.height();
|
||||||
|
let text_size = egui::TextStyle::Body.resolve(ui.style()).size;
|
||||||
|
let text_color = self.fg_stroke(visuals).color;
|
||||||
|
|
||||||
|
// Draw day names
|
||||||
|
for (i, name) in ["Pir", "Ant", "Tre", "Ket", "Pen"].iter().enumerate() {
|
||||||
|
let offset = column_width * (i as f32 + 0.5);
|
||||||
|
|
||||||
|
painter.text(
|
||||||
|
rect.left_top() + vec2(offset, header_size/2.5),
|
||||||
|
Align2::CENTER_CENTER,
|
||||||
|
name,
|
||||||
|
FontId::monospace(text_size*1.2),
|
||||||
|
text_color
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw dates
|
||||||
|
let year = week.year();
|
||||||
|
let week = week.week();
|
||||||
|
let mut week_date = NaiveDate::from_isoywd_opt(year, week, Weekday::Mon).expect("Invalid week or year given");
|
||||||
|
for i in 1..=5 {
|
||||||
|
let offset = column_width * i as f32;
|
||||||
|
|
||||||
|
painter.text(
|
||||||
|
rect.left_top() + vec2(offset-3.0, header_size-3.0),
|
||||||
|
Align2::RIGHT_BOTTOM,
|
||||||
|
week_date.format("%m-%d").to_string(),
|
||||||
|
FontId::proportional(text_size*0.85),
|
||||||
|
text_color
|
||||||
|
);
|
||||||
|
week_date = week_date.add(Duration::days(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_body(
|
||||||
|
&self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
rect: Rect,
|
||||||
|
week: IsoWeek,
|
||||||
|
now: NaiveDateTime
|
||||||
|
) {
|
||||||
|
use egui::*;
|
||||||
|
|
||||||
|
let painter = ui.painter();
|
||||||
|
let column_width = rect.width()/5.0;
|
||||||
|
let column_gap = 3.0;
|
||||||
|
|
||||||
|
let visuals = ui.visuals();
|
||||||
|
let bg_fill = self.bg_fill(visuals);
|
||||||
|
let dark_bg_fill = self.dark_bg_fill(visuals);
|
||||||
|
let highlight_color = self.highlight_color(visuals);
|
||||||
|
|
||||||
|
let timestamps = ["9:00", "10:30", "11:00", "12:30", "13:30", "15:00", "15:30", "17:00"];
|
||||||
|
let timestamps_mins = timestamps.map(count_minutes);
|
||||||
|
let total_minutes = timestamps_mins.last().unwrap() - timestamps_mins.first().unwrap();
|
||||||
|
let minute_to_pixel_scale = rect.height()/total_minutes as f32;
|
||||||
|
|
||||||
|
// draw bg
|
||||||
|
painter.rect_filled(
|
||||||
|
rect,
|
||||||
|
Rounding::none(),
|
||||||
|
bg_fill
|
||||||
|
);
|
||||||
|
|
||||||
|
// Highlight current day column
|
||||||
|
if now.iso_week() == week && !is_weekend(now.date()) {
|
||||||
|
let days_from_monday = now.weekday().num_days_from_monday() as f32;
|
||||||
|
let rect = Rect::from_min_max(
|
||||||
|
rect.left_top() + vec2(column_width * days_from_monday, 0.0),
|
||||||
|
rect.left_bottom() + vec2(column_width * (1.0+days_from_monday), 0.0)
|
||||||
|
);
|
||||||
|
painter.rect_filled(rect, Rounding::none(), highlight_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw gaps between columns
|
||||||
|
for i in 1..5 {
|
||||||
|
let offset = column_width * i as f32;
|
||||||
|
painter.line_segment([
|
||||||
|
rect.left_top() + vec2(offset, 0.0),
|
||||||
|
rect.left_bottom() + vec2(offset, 0.0)
|
||||||
|
], (column_gap, dark_bg_fill))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark break times
|
||||||
|
for i in (1..timestamps_mins.len()-1).step_by(2) {
|
||||||
|
let from = (timestamps_mins[i] - timestamps_mins[0]) as f32 * minute_to_pixel_scale;
|
||||||
|
let to = (timestamps_mins[i+1] - timestamps_mins[0]) as f32 * minute_to_pixel_scale;
|
||||||
|
draw_repeating_texture(
|
||||||
|
ui,
|
||||||
|
self.break_texture.as_ref(),
|
||||||
|
Rect::from_min_size(
|
||||||
|
rect.left_top() + vec2(0.0, from),
|
||||||
|
vec2(rect.width(), to - from)
|
||||||
|
),
|
||||||
|
dark_bg_fill
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw event cards
|
||||||
|
for event in self.events {
|
||||||
|
let day = event.date.weekday().num_days_from_monday() as usize;
|
||||||
|
let duration = (event.end_time - event.start_time).num_minutes() as f32;
|
||||||
|
let start_time = event.start_time.hour()*60 + event.start_time.minute() - timestamps_mins[0];
|
||||||
|
let event_rect = Rect::from_min_size(
|
||||||
|
rect.left_top() + vec2(column_width*day as f32, start_time as f32*minute_to_pixel_scale),
|
||||||
|
vec2(column_width, duration*minute_to_pixel_scale)
|
||||||
|
).shrink2(vec2(10.0, 0.0));
|
||||||
|
self.show_event(ui, event, event_rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now line
|
||||||
|
let painter = ui.painter();
|
||||||
|
let current_time = now.minute() + now.hour() * 60 - timestamps_mins[0];
|
||||||
|
if current_time > 0 && current_time < *timestamps_mins.last().unwrap_or(&0) && !is_weekend(now.date()) {
|
||||||
|
let offset = current_time as f32 * minute_to_pixel_scale;
|
||||||
|
let points = [
|
||||||
|
rect.left_top() + vec2(0.0, offset),
|
||||||
|
rect.right_top() + vec2(0.0, offset)
|
||||||
|
];
|
||||||
|
let thickness = 2.0;
|
||||||
|
let border_size = 2.0;
|
||||||
|
painter.line_segment(points, (thickness + 2.0 * border_size, dark_bg_fill));
|
||||||
|
painter.line_segment(points, (thickness, highlight_color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for EventsTable<'a> {
|
||||||
|
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
||||||
|
use egui::*;
|
||||||
|
|
||||||
|
let response = ui.allocate_rect(ui.min_rect(), Sense::hover());
|
||||||
|
let rect = response.rect;
|
||||||
|
|
||||||
|
let now = self.now.unwrap_or(Local::now().naive_local());
|
||||||
|
let week = self.week.unwrap_or(now.iso_week());
|
||||||
|
|
||||||
|
self.show_header(
|
||||||
|
ui,
|
||||||
|
Rect::from_min_size(
|
||||||
|
rect.left_top(),
|
||||||
|
vec2(rect.width(), HEADER_SIZE)
|
||||||
|
),
|
||||||
|
week
|
||||||
|
);
|
||||||
|
|
||||||
|
self.show_body(
|
||||||
|
ui,
|
||||||
|
Rect::from_min_max(
|
||||||
|
rect.left_top() + vec2(0.0, HEADER_SIZE),
|
||||||
|
rect.right_bottom()
|
||||||
|
),
|
||||||
|
week,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
39
src/main.rs
39
src/main.rs
@ -1,12 +1,19 @@
|
|||||||
#![windows_subsystem = "windows"]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
mod timetable;
|
mod timetable;
|
||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod events_table;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
use app::MainApp;
|
use app::MainApp;
|
||||||
use config::ConfigStorage;
|
use chrono::{Local, NaiveDate, NaiveTime};
|
||||||
|
use config::{MemoryConfigStore, Config, TomlConfigStore};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
use timetable::{DummyTimetableGetter, Timetable, Event, BlockingTimetableGetter};
|
||||||
|
|
||||||
// TODO: use lazy_static!() to load assets
|
// TODO: use lazy_static!() to load assets
|
||||||
// TODO: convert events_table to egui widget
|
// TODO: convert events_table to egui widget
|
||||||
@ -14,10 +21,29 @@ use eframe::egui;
|
|||||||
// TODO: Settings menu
|
// TODO: Settings menu
|
||||||
// TODO: use "confy" for config loading?
|
// TODO: use "confy" for config loading?
|
||||||
// TODO: refactor persistence
|
// TODO: refactor persistence
|
||||||
|
// TODO: Setup pipeline
|
||||||
|
|
||||||
fn main() -> Result<(), ureq::Error> {
|
fn main() -> Result<(), ureq::Error> {
|
||||||
let mut config_storage = ConfigStorage::default();
|
let config_store = TomlConfigStore::default();
|
||||||
config_storage.config.vidko_code = Some("E1810".into());
|
// let config_store = MemoryConfigStore::new(Config {
|
||||||
|
// vidko: None//Some("E1810".into())
|
||||||
|
// });
|
||||||
|
|
||||||
|
let timetable_getter = BlockingTimetableGetter::default();
|
||||||
|
// let timetable_getter = DummyTimetableGetter::new(Timetable {
|
||||||
|
// events: vec![
|
||||||
|
// Event {
|
||||||
|
// category: timetable::EventCategory::Default,
|
||||||
|
// date: NaiveDate::from_ymd_opt(2023, 1, 30).unwrap(),
|
||||||
|
// start_time: NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||||||
|
// end_time: NaiveTime::from_hms_opt(10, 30, 0).unwrap(),
|
||||||
|
// description: "Foobarbaz".into(),
|
||||||
|
// summary: "P123B123 Dummy module".into(),
|
||||||
|
// location: "XI r.-521".into(),
|
||||||
|
// module_name: Some("Dummy module".into())
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// });
|
||||||
|
|
||||||
let mut native_options = eframe::NativeOptions::default();
|
let mut native_options = eframe::NativeOptions::default();
|
||||||
native_options.decorated = true;
|
native_options.decorated = true;
|
||||||
@ -32,14 +58,13 @@ fn main() -> Result<(), ureq::Error> {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
});
|
});
|
||||||
let mut app = MainApp::new(config_storage);
|
let mut app = MainApp::new(Box::new(config_store), Box::new(timetable_getter));
|
||||||
|
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"KTU timetable",
|
"KTU timetable",
|
||||||
native_options,
|
native_options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
app.on_creation(cc);
|
app.init(cc);
|
||||||
app.refresh_timetable();
|
|
||||||
Box::new(app)
|
Box::new(app)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
131
src/timetable.rs
131
src/timetable.rs
@ -2,7 +2,7 @@ use ical::property::Property;
|
|||||||
use std::{error::Error, fmt};
|
use std::{error::Error, fmt};
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use chrono::{NaiveDate, NaiveTime, IsoWeek, Datelike};
|
use chrono::{NaiveDate, NaiveTime, IsoWeek, Datelike};
|
||||||
use lazy_regex::{regex, regex_captures};
|
use lazy_regex::{regex_captures};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum EventCategory {
|
pub enum EventCategory {
|
||||||
@ -23,11 +23,9 @@ pub struct Event {
|
|||||||
pub module_name: Option<String>
|
pub module_name: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make errors more descriptive
|
#[derive(Debug, Clone)]
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Timetable {
|
pub struct Timetable {
|
||||||
events: Vec<Event>
|
pub events: Vec<Event>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -53,73 +51,100 @@ impl Error for GetTimetableError {}
|
|||||||
|
|
||||||
impl fmt::Display for GetTimetableError {
|
impl fmt::Display for GetTimetableError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "Failed to get timetable")
|
match self {
|
||||||
|
GetTimetableError::NotFound => write!(f, "Timetable not found"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn guess_module_name(summary: &str) -> Option<String> {
|
fn guess_module_name(summary: &str) -> Option<String> {
|
||||||
let captures = regex_captures!(r"^P\d{3}B\d{3} (.+)", summary);
|
let captures = regex_captures!(r"^\w\d{3}\w\d{3} (.+)", summary);
|
||||||
if let Some((_, module_name)) = captures {
|
if let Some((_, module_name)) = captures {
|
||||||
return Some(module_name.into());
|
return Some(module_name.into());
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_timetable(vidko: &str) -> Result<Timetable, GetTimetableError> {
|
pub trait TimetableGetter {
|
||||||
fn find_property<'a>(properties: &'a [Property], name: &str) -> Result<&'a Property, GetTimetableError> {
|
fn get(&self, vidko: &str) -> Result<Timetable, GetTimetableError>;
|
||||||
for prop in properties {
|
}
|
||||||
if prop.name == name {
|
|
||||||
return Ok(prop);
|
#[derive(Default)]
|
||||||
|
pub struct BlockingTimetableGetter {}
|
||||||
|
impl TimetableGetter for BlockingTimetableGetter {
|
||||||
|
fn get(&self, vidko: &str) -> Result<Timetable, GetTimetableError> {
|
||||||
|
fn find_property<'a>(properties: &'a [Property], name: &str) -> Result<&'a Property, GetTimetableError> {
|
||||||
|
for prop in properties {
|
||||||
|
if prop.name == name {
|
||||||
|
return Ok(prop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
panic!("Property '{}' not found", name);
|
||||||
}
|
}
|
||||||
panic!("Property '{}' not found", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp = ureq::get(&format!("https://uais.cr.ktu.lt/ktuis/tv_rprt2.ical1?p={}&t=basic.ics", vidko))
|
let resp = ureq::get(&format!("https://uais.cr.ktu.lt/ktuis/tv_rprt2.ical1?p={}&t=basic.ics", vidko))
|
||||||
.call()
|
.call()
|
||||||
.map_err(|_| GetTimetableError::NotFound)?;
|
.map_err(|_| GetTimetableError::NotFound)?;
|
||||||
|
|
||||||
let mut reader = ical::IcalParser::new(BufReader::new(resp.into_reader()));
|
let mut reader = ical::IcalParser::new(BufReader::new(resp.into_reader()));
|
||||||
let cal = reader.next();
|
let cal = reader.next();
|
||||||
if cal.is_none() {
|
if cal.is_none() {
|
||||||
return Err(GetTimetableError::NotFound)
|
return Err(GetTimetableError::NotFound)
|
||||||
}
|
}
|
||||||
let cal = cal.unwrap().unwrap();
|
let cal = cal.unwrap().unwrap();
|
||||||
|
if cal.events.is_empty() {
|
||||||
|
return Err(GetTimetableError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
let mut timetable = Timetable { events: vec![] };
|
let mut timetable = Timetable { events: vec![] };
|
||||||
for event in cal.events {
|
for event in cal.events {
|
||||||
let category_prop = find_property(&event.properties, "CATEGORIES")?;
|
let category_prop = find_property(&event.properties, "CATEGORIES")?;
|
||||||
let start_prop = find_property(&event.properties, "DTSTART")?;
|
let start_prop = find_property(&event.properties, "DTSTART")?;
|
||||||
let end_prop = find_property(&event.properties, "DTEND")?;
|
let end_prop = find_property(&event.properties, "DTEND")?;
|
||||||
let description_prop = find_property(&event.properties, "DESCRIPTION")?;
|
let description_prop = find_property(&event.properties, "DESCRIPTION")?;
|
||||||
let summary_prop = find_property(&event.properties, "SUMMARY")?;
|
let summary_prop = find_property(&event.properties, "SUMMARY")?;
|
||||||
let location_prop = find_property(&event.properties, "LOCATION")?;
|
let location_prop = find_property(&event.properties, "LOCATION")?;
|
||||||
|
|
||||||
let mut category = EventCategory::Default;
|
let mut category = EventCategory::Default;
|
||||||
if let Some(category_value) = &category_prop.value {
|
if let Some(category_value) = &category_prop.value {
|
||||||
if category_value == "Yellow Category" {
|
if category_value == "Yellow Category" {
|
||||||
category = EventCategory::Yellow;
|
category = EventCategory::Yellow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
let start_str = start_prop.value.clone().unwrap();
|
||||||
|
let end_str = end_prop.value.clone().unwrap();
|
||||||
|
let (start_date, start_time) = start_str.split_once('T').unwrap();
|
||||||
|
let (_end_date, end_time) = end_str.split_once('T').unwrap();
|
||||||
|
let summary = summary_prop.value.clone().unwrap();
|
||||||
|
|
||||||
|
timetable.events.push(Event {
|
||||||
|
category,
|
||||||
|
date: NaiveDate::parse_from_str(start_date, "%Y%m%d").unwrap(),
|
||||||
|
start_time: NaiveTime::parse_from_str(start_time, "%H%M%S").unwrap(),
|
||||||
|
end_time: NaiveTime::parse_from_str(end_time, "%H%M%S").unwrap(),
|
||||||
|
description: description_prop.value.clone().unwrap(),
|
||||||
|
module_name: guess_module_name(&summary),
|
||||||
|
summary,
|
||||||
|
location: location_prop.value.clone().unwrap()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
let start_str = start_prop.value.clone().unwrap();
|
|
||||||
let end_str = end_prop.value.clone().unwrap();
|
|
||||||
let (start_date, start_time) = start_str.split_once('T').unwrap();
|
|
||||||
let (_end_date, end_time) = end_str.split_once('T').unwrap();
|
|
||||||
let summary = summary_prop.value.clone().unwrap();
|
|
||||||
|
|
||||||
timetable.events.push(Event {
|
timetable.events.sort_by_key(|event| (event.date, event.start_time));
|
||||||
category,
|
|
||||||
date: NaiveDate::parse_from_str(start_date, "%Y%m%d").unwrap(),
|
Ok(timetable)
|
||||||
start_time: NaiveTime::parse_from_str(start_time, "%H%M%S").unwrap(),
|
|
||||||
end_time: NaiveTime::parse_from_str(end_time, "%H%M%S").unwrap(),
|
|
||||||
description: description_prop.value.clone().unwrap(),
|
|
||||||
module_name: guess_module_name(&summary),
|
|
||||||
summary,
|
|
||||||
location: location_prop.value.clone().unwrap()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timetable.events.sort_by_key(|event| (event.date, event.start_time));
|
pub struct DummyTimetableGetter {
|
||||||
|
timetable: Timetable
|
||||||
Ok(timetable)
|
}
|
||||||
|
impl DummyTimetableGetter {
|
||||||
|
pub fn new(timetable: Timetable) -> Self {
|
||||||
|
Self { timetable }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TimetableGetter for DummyTimetableGetter {
|
||||||
|
fn get(&self, vidko: &str) -> Result<Timetable, GetTimetableError> {
|
||||||
|
Ok(self.timetable.clone())
|
||||||
|
}
|
||||||
}
|
}
|
12
src/utils.rs
Normal file
12
src/utils.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use egui::ColorImage;
|
||||||
|
|
||||||
|
pub fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
|
||||||
|
let image = image::load_from_memory(image_data)?;
|
||||||
|
let size = [image.width() as _, image.height() as _];
|
||||||
|
let image_buffer = image.to_rgba8();
|
||||||
|
let pixels = image_buffer.as_flat_samples();
|
||||||
|
Ok(ColorImage::from_rgba_unmultiplied(
|
||||||
|
size,
|
||||||
|
pixels.as_slice(),
|
||||||
|
))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user