mirror of
https://github.com/FliegendeWurst/raspi-oled.git
synced 2024-11-25 03:34:58 +00:00
New Github notifications thingy
This commit is contained in:
parent
ad9cea9378
commit
efac4f49a7
1010
Cargo.lock
generated
1010
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@ embedded-graphics = "0.8.1"
|
||||
linux-embedded-hal = "0.3.0"
|
||||
embedded-hal = "0.2.5"
|
||||
libc = "0.2.98"
|
||||
rusqlite = "0.27.0"
|
||||
rusqlite = "0.32.1"
|
||||
time = { version = "0.3.9", features = ["parsing"] }
|
||||
time-tz = "2"
|
||||
image = { version = "0.24.1", optional = true }
|
||||
@ -21,7 +21,7 @@ serde = "1.0.136"
|
||||
rppal = { version = "0.14.1", features = ["hal"] }
|
||||
ssd1351 = { git = "https://github.com/FliegendeWurst/ssd1351-rust", rev = "3de5be50bd9a59391c669aec8357923a56d121f6" }
|
||||
display-interface-spi = "0.4.1"
|
||||
ureq = { version = "2.4.0", default-features = false }
|
||||
ureq = { version = "2.4.0", default-features = false, features = ["tls"] }
|
||||
winit = { version = "0.28.7", optional = true }
|
||||
softbuffer = { version = "0.3.1", optional = true }
|
||||
rand_xoshiro = "0.6.0"
|
||||
@ -29,6 +29,7 @@ gpiocdev = "0.6.0"
|
||||
rpassword = "7.2.0"
|
||||
andotp-import = "0.1.0"
|
||||
totp-rs = "5.4.0"
|
||||
color_space = "0.5.4"
|
||||
#gpio-am2302-rs = { git = "https://github.com/FliegendeWurst/gpio-am2302-rs" }
|
||||
|
||||
[features]
|
||||
|
@ -321,8 +321,8 @@ impl<D: DrawTarget<Color = Rgb565>> Draw<D> for Measurements {
|
||||
}
|
||||
if time_until_first.is_none()
|
||||
&& (i > 0
|
||||
|| event.1 > time.hour() as i32 || (event.1 == time.hour() as i32
|
||||
&& event.2 >= time.minute() as i32))
|
||||
|| event.1 > time.hour() as i32
|
||||
|| (event.1 == time.hour() as i32 && event.2 >= time.minute() as i32))
|
||||
{
|
||||
time_until_first = Some(
|
||||
((i * 24 + event.1) * 60 + event.2) * 60
|
||||
|
@ -22,7 +22,7 @@ use rppal::{
|
||||
spi::{Bus, Mode, SlaveSelect, Spi},
|
||||
};
|
||||
use rusqlite::Connection;
|
||||
use schedule::Schedule;
|
||||
use schedule::{github_notifications::GithubNotifications, Schedule};
|
||||
use screensaver::{Screensaver, TimeDisplay};
|
||||
use ssd1351::display::display::Ssd1351;
|
||||
use time::OffsetDateTime;
|
||||
@ -37,6 +37,8 @@ pub type Oled = Ssd1351<SPIInterfaceNoCS<Spi, OutputPin>>;
|
||||
pub type Rng = Xoroshiro128StarStar;
|
||||
|
||||
static BLACK: Rgb565 = Rgb565::new(0, 0, 0);
|
||||
/// Delay after drawing a frame in milliseconds.
|
||||
const FRAME_INTERVAL: u64 = 66;
|
||||
|
||||
fn main() {
|
||||
if rppal::system::DeviceInfo::new().is_ok() {
|
||||
@ -46,7 +48,9 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Context {
|
||||
pub trait Context<D: DrawTarget<Color = Rgb565>> {
|
||||
fn do_draw(&self, drawable: Box<dyn Draw<D>>);
|
||||
|
||||
fn do_action(&self, action: Action);
|
||||
|
||||
fn active_count(&self) -> usize;
|
||||
@ -58,7 +62,7 @@ pub trait Context {
|
||||
|
||||
pub struct ContextDefault<D: DrawTarget<Color = Rgb565>> {
|
||||
screensavers: Vec<Box<dyn Screensaver<D>>>,
|
||||
scheduled: Vec<Box<dyn Schedule>>,
|
||||
scheduled: Vec<Box<dyn Schedule<D>>>,
|
||||
active: RefCell<Vec<Box<dyn Draw<D>>>>,
|
||||
database: Rc<RefCell<Connection>>,
|
||||
}
|
||||
@ -70,10 +74,16 @@ impl<D: DrawTarget<Color = Rgb565>> ContextDefault<D> {
|
||||
screensavers.push(Box::new(draw::Measurements::temps()));
|
||||
screensavers.push(Box::new(draw::Measurements::events()));
|
||||
let database = Connection::open("sensors.db").expect("failed to open database");
|
||||
let mut scheduled = schedule::reminders();
|
||||
scheduled.push(Box::new(GithubNotifications {
|
||||
pat: env::var("GITHUB_PAT").expect("no env var GITHUB_PAT set"),
|
||||
last_modified: RefCell::new(None),
|
||||
last_call: RefCell::new(OffsetDateTime::now_utc().to_timezone(BERLIN) - time::Duration::seconds(50)),
|
||||
}));
|
||||
ContextDefault {
|
||||
database: Rc::new(RefCell::new(database)),
|
||||
screensavers,
|
||||
scheduled: schedule::reminders(),
|
||||
scheduled,
|
||||
active: RefCell::new(vec![Box::new(TimeDisplay::new())]),
|
||||
}
|
||||
}
|
||||
@ -112,7 +122,11 @@ impl<D: DrawTarget<Color = Rgb565>> ContextDefault<D> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: DrawTarget<Color = Rgb565>> Context for ContextDefault<D> {
|
||||
impl<D: DrawTarget<Color = Rgb565>> Context<D> for ContextDefault<D> {
|
||||
fn do_draw(&self, drawable: Box<dyn Draw<D>>) {
|
||||
self.active.borrow_mut().push(drawable);
|
||||
}
|
||||
|
||||
fn do_action(&self, action: Action) {
|
||||
match action {
|
||||
Action::Screensaver(id) => {
|
||||
@ -226,7 +240,7 @@ fn pc_main() {
|
||||
.unwrap();
|
||||
|
||||
// redraw
|
||||
if Instant::now().duration_since(start) > Duration::from_millis(iters * 66) {
|
||||
if Instant::now().duration_since(start) > Duration::from_millis(iters * FRAME_INTERVAL) {
|
||||
iters += 1;
|
||||
buffer_dirty = ctx.loop_iter(&mut disp, &mut rng);
|
||||
}
|
||||
@ -428,6 +442,6 @@ fn main_loop(mut disp: Oled, mut ctx: ContextDefault<Oled>) {
|
||||
if dirty {
|
||||
let _ = disp.flush(); // ignore bus write errors, they are harmless
|
||||
}
|
||||
thread::sleep(Duration::from_millis(66));
|
||||
thread::sleep(Duration::from_millis(FRAME_INTERVAL));
|
||||
}
|
||||
}
|
||||
|
117
src/bin/schedule/github_notifications.rs
Normal file
117
src/bin/schedule/github_notifications.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use color_space::{Hsv, ToRgb};
|
||||
use embedded_graphics::{
|
||||
mono_font::{iso_8859_10::FONT_8X13, MonoTextStyleBuilder},
|
||||
pixelcolor::Rgb565,
|
||||
prelude::{DrawTarget, Point, RgbColor},
|
||||
text::Text,
|
||||
Drawable,
|
||||
};
|
||||
use raspi_oled::github::get_new_notifications;
|
||||
|
||||
use crate::{
|
||||
screensaver::{SimpleScreensaver, GITHUB},
|
||||
Draw,
|
||||
};
|
||||
|
||||
use super::Schedule;
|
||||
|
||||
pub struct GithubNotifications {
|
||||
pub pat: String,
|
||||
pub last_modified: RefCell<Option<String>>,
|
||||
pub last_call: RefCell<time::OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl<D: DrawTarget<Color = Rgb565>> Schedule<D> for GithubNotifications {
|
||||
fn check(&self, _ctx: &dyn crate::Context<D>, time: time::OffsetDateTime) -> bool {
|
||||
let time_since_last = time - *self.last_call.borrow();
|
||||
time_since_last.whole_minutes() >= 1
|
||||
}
|
||||
|
||||
fn execute(&self, ctx: &dyn crate::Context<D>, time: time::OffsetDateTime) {
|
||||
*self.last_call.borrow_mut() = time;
|
||||
let last_modified = self.last_modified.borrow().clone();
|
||||
if let Ok((notifications, last_modified)) = get_new_notifications(&self.pat, last_modified.as_deref()) {
|
||||
*self.last_modified.borrow_mut() = last_modified;
|
||||
let relevant: Vec<_> = notifications
|
||||
.into_iter()
|
||||
.filter(|x| x.reason != "state_change" && x.unread)
|
||||
.collect();
|
||||
if relevant.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut lines = vec![];
|
||||
let mut relevant = relevant.into_iter();
|
||||
for _ in 0..8 {
|
||||
if let Some(x) = relevant.next() {
|
||||
let url = x.subject.url;
|
||||
let Some(url) = url else {
|
||||
lines.push("no url".to_owned());
|
||||
continue;
|
||||
};
|
||||
let parts: Vec<_> = url.split('/').collect();
|
||||
if parts.len() < 8 {
|
||||
lines.push("too few url parts".to_owned());
|
||||
continue;
|
||||
}
|
||||
lines.push(format!("{} #{}", parts[5], parts[7]));
|
||||
}
|
||||
}
|
||||
let remaining = relevant.count();
|
||||
if remaining != 0 {
|
||||
lines.push(format!("... {} more", remaining));
|
||||
}
|
||||
ctx.do_draw(Box::new(GithubNotificationsDraw {
|
||||
calls: RefCell::new(0),
|
||||
screen: &GITHUB,
|
||||
lines,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GithubNotificationsDraw {
|
||||
calls: RefCell<usize>,
|
||||
screen: &'static SimpleScreensaver,
|
||||
lines: Vec<String>,
|
||||
}
|
||||
|
||||
impl<D: DrawTarget<Color = Rgb565>> Draw<D> for GithubNotificationsDraw {
|
||||
fn draw(&self, disp: &mut D, _rng: &mut crate::Rng) -> Result<bool, D::Error> {
|
||||
let calls = *self.calls.borrow();
|
||||
if calls < 40 {
|
||||
let hue = calls as f64 / 40.0 * 360.0;
|
||||
let hsv = Hsv::new(hue, 1.0, 1.0);
|
||||
let rgb = hsv.to_rgb();
|
||||
let r = rgb.r as u8 >> 3;
|
||||
let g = rgb.g as u8 >> 2;
|
||||
let b = rgb.b as u8 >> 3;
|
||||
self.screen.draw_all(disp, Rgb565::new(r, g, b))?;
|
||||
} else {
|
||||
disp.clear(Rgb565::BLACK)?;
|
||||
// fit 9 lines
|
||||
let text_style_clock = MonoTextStyleBuilder::new()
|
||||
.font(&FONT_8X13)
|
||||
.text_color(Rgb565::WHITE)
|
||||
.build();
|
||||
for (y, line) in self.lines.iter().enumerate() {
|
||||
Text::new(line, Point::new(0, (12 + y * 14) as _), text_style_clock).draw(disp)?;
|
||||
}
|
||||
}
|
||||
*self.calls.borrow_mut() += 1;
|
||||
Ok(calls < 90)
|
||||
}
|
||||
|
||||
fn expired(&self) -> bool {
|
||||
*self.calls.borrow() > 90
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
&*self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
&mut *self
|
||||
}
|
||||
}
|
@ -1,18 +1,24 @@
|
||||
use embedded_graphics::{pixelcolor::Rgb565, prelude::DrawTarget};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{action::Action, Context};
|
||||
|
||||
pub mod github_notifications;
|
||||
|
||||
/// Task to be executed at certain times.
|
||||
/// Guaranteed to be checked at least once every minute.
|
||||
pub trait Schedule {
|
||||
fn check_and_do(&self, ctx: &dyn Context, time: OffsetDateTime) {
|
||||
pub trait Schedule<D>
|
||||
where
|
||||
D: DrawTarget<Color = Rgb565>,
|
||||
{
|
||||
fn check_and_do(&self, ctx: &dyn Context<D>, time: OffsetDateTime) {
|
||||
if self.check(ctx, time) {
|
||||
self.execute(ctx, time);
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&self, ctx: &dyn Context, time: OffsetDateTime) -> bool;
|
||||
fn execute(&self, ctx: &dyn Context, time: OffsetDateTime);
|
||||
fn check(&self, ctx: &dyn Context<D>, time: OffsetDateTime) -> bool;
|
||||
fn execute(&self, ctx: &dyn Context<D>, time: OffsetDateTime);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@ -23,12 +29,12 @@ pub struct Reminder {
|
||||
should_beep: bool,
|
||||
}
|
||||
|
||||
impl Schedule for Reminder {
|
||||
fn check(&self, ctx: &dyn Context, time: OffsetDateTime) -> bool {
|
||||
impl<D: DrawTarget<Color = Rgb565>> Schedule<D> for Reminder {
|
||||
fn check(&self, ctx: &dyn Context<D>, time: OffsetDateTime) -> bool {
|
||||
time.hour() == self.hour && time.minute() == self.minute && ctx.active_count() == 1
|
||||
}
|
||||
|
||||
fn execute(&self, ctx: &dyn Context, _time: OffsetDateTime) {
|
||||
fn execute(&self, ctx: &dyn Context<D>, _time: OffsetDateTime) {
|
||||
if self.should_beep {
|
||||
ctx.enable_pwm();
|
||||
}
|
||||
@ -51,6 +57,6 @@ static DUOLINGO: Reminder = Reminder::new(11, 40, Action::Screensaver("duolingo"
|
||||
static DUOLINGO_NIGHT: Reminder = Reminder::new(23, 40, Action::Screensaver("duolingo"), false);
|
||||
static FOOD: Reminder = Reminder::new(13, 15, Action::Screensaver("plate"), false);
|
||||
|
||||
pub fn reminders() -> Vec<Box<dyn Schedule>> {
|
||||
pub fn reminders<D: DrawTarget<Color = Rgb565>>() -> Vec<Box<dyn Schedule<D>>> {
|
||||
vec![Box::new(DUOLINGO), Box::new(DUOLINGO_NIGHT), Box::new(FOOD)]
|
||||
}
|
||||
|
BIN
src/bin/screensaver/github.raw
Normal file
BIN
src/bin/screensaver/github.raw
Normal file
Binary file not shown.
@ -3,6 +3,7 @@ use std::cell::RefCell;
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64};
|
||||
|
||||
use embedded_graphics::mono_font::ascii::FONT_10X20;
|
||||
use embedded_graphics::prelude::RgbColor;
|
||||
use embedded_graphics::{
|
||||
mono_font::MonoTextStyleBuilder,
|
||||
pixelcolor::Rgb565,
|
||||
@ -102,6 +103,24 @@ impl SimpleScreensaver {
|
||||
iters: AtomicU32::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_all<D: DrawTarget<Color = Rgb565>>(&self, disp: &mut D, color: Rgb565) -> Result<(), D::Error> {
|
||||
disp.fill_contiguous(
|
||||
&Rectangle::new((0, 0).into(), (128, 128).into()),
|
||||
(0..128 * 128).map(|idx| {
|
||||
let (red, green, blue) = (self.data[3 * idx], self.data[3 * idx + 1], self.data[3 * idx + 2]);
|
||||
let r = red >> 3;
|
||||
let g = green >> 2;
|
||||
let b = blue >> 3;
|
||||
if (r, g, b) != (0, 0, 0) {
|
||||
color
|
||||
} else {
|
||||
Rgb565::BLACK
|
||||
}
|
||||
}),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
static TIME_COLOR: Rgb565 = Rgb565::new(0b01_111, 0b011_111, 0b01_111);
|
||||
@ -195,6 +214,7 @@ pub static RPI: SimpleScreensaver = SimpleScreensaver::new("rpi", include_bytes!
|
||||
pub static DUOLINGO: SimpleScreensaver = SimpleScreensaver::new("duolingo", include_bytes!("./duolingo.raw"));
|
||||
pub static SPAGHETTI: SimpleScreensaver = SimpleScreensaver::new("spaghetti", include_bytes!("./spaghetti.raw"));
|
||||
pub static PLATE: SimpleScreensaver = SimpleScreensaver::new("plate", include_bytes!("./plate.raw"));
|
||||
pub static GITHUB: SimpleScreensaver = SimpleScreensaver::new("github", include_bytes!("./github.raw"));
|
||||
|
||||
pub fn screensavers<D: DrawTarget<Color = Rgb565>>() -> Vec<Box<dyn Screensaver<D>>> {
|
||||
vec![
|
||||
|
215
src/github.rs
Normal file
215
src/github.rs
Normal file
@ -0,0 +1,215 @@
|
||||
use std::error::Error;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Notification {
|
||||
pub id: String,
|
||||
pub repository: Repository,
|
||||
pub subject: Subject,
|
||||
pub reason: String,
|
||||
pub unread: bool,
|
||||
pub updated_at: String,
|
||||
pub last_read_at: Option<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Repository {
|
||||
pub id: u64,
|
||||
|
||||
pub node_id: Option<String>,
|
||||
pub name: String,
|
||||
|
||||
pub full_name: Option<String>,
|
||||
|
||||
pub owner: Option<serde_json::Value>,
|
||||
|
||||
pub private: Option<bool>,
|
||||
|
||||
pub html_url: Option<String>,
|
||||
|
||||
pub description: Option<String>,
|
||||
|
||||
pub fork: Option<bool>,
|
||||
pub url: String,
|
||||
|
||||
pub archive_url: Option<String>,
|
||||
|
||||
pub assignees_url: Option<String>,
|
||||
|
||||
pub blobs_url: Option<String>,
|
||||
|
||||
pub branches_url: Option<String>,
|
||||
|
||||
pub collaborators_url: Option<String>,
|
||||
|
||||
pub comments_url: Option<String>,
|
||||
|
||||
pub commits_url: Option<String>,
|
||||
|
||||
pub compare_url: Option<String>,
|
||||
|
||||
pub contents_url: Option<String>,
|
||||
|
||||
pub contributors_url: Option<String>,
|
||||
|
||||
pub deployments_url: Option<String>,
|
||||
|
||||
pub downloads_url: Option<String>,
|
||||
|
||||
pub events_url: Option<String>,
|
||||
|
||||
pub forks_url: Option<String>,
|
||||
|
||||
pub git_commits_url: Option<String>,
|
||||
|
||||
pub git_refs_url: Option<String>,
|
||||
|
||||
pub git_tags_url: Option<String>,
|
||||
|
||||
pub git_url: Option<String>,
|
||||
|
||||
pub issue_comment_url: Option<String>,
|
||||
|
||||
pub issue_events_url: Option<String>,
|
||||
|
||||
pub issues_url: Option<String>,
|
||||
|
||||
pub keys_url: Option<String>,
|
||||
|
||||
pub labels_url: Option<String>,
|
||||
|
||||
pub languages_url: Option<String>,
|
||||
|
||||
pub merges_url: Option<String>,
|
||||
|
||||
pub milestones_url: Option<String>,
|
||||
|
||||
pub notifications_url: Option<String>,
|
||||
|
||||
pub pulls_url: Option<String>,
|
||||
|
||||
pub releases_url: Option<String>,
|
||||
|
||||
pub ssh_url: Option<String>,
|
||||
|
||||
pub stargazers_url: Option<String>,
|
||||
|
||||
pub statuses_url: Option<String>,
|
||||
|
||||
pub subscribers_url: Option<String>,
|
||||
|
||||
pub subscription_url: Option<String>,
|
||||
|
||||
pub tags_url: Option<String>,
|
||||
|
||||
pub teams_url: Option<String>,
|
||||
|
||||
pub trees_url: Option<String>,
|
||||
|
||||
pub clone_url: Option<String>,
|
||||
|
||||
pub mirror_url: Option<String>,
|
||||
|
||||
pub hooks_url: Option<String>,
|
||||
|
||||
pub svn_url: Option<String>,
|
||||
|
||||
pub homepage: Option<String>,
|
||||
|
||||
pub language: Option<::serde_json::Value>,
|
||||
|
||||
pub forks_count: Option<u32>,
|
||||
|
||||
pub stargazers_count: Option<u32>,
|
||||
|
||||
pub watchers_count: Option<u32>,
|
||||
|
||||
pub size: Option<u32>,
|
||||
|
||||
pub default_branch: Option<String>,
|
||||
|
||||
pub open_issues_count: Option<u32>,
|
||||
|
||||
pub is_template: Option<bool>,
|
||||
|
||||
pub topics: Option<Vec<String>>,
|
||||
|
||||
pub has_issues: Option<bool>,
|
||||
|
||||
pub has_projects: Option<bool>,
|
||||
|
||||
pub has_wiki: Option<bool>,
|
||||
|
||||
pub has_pages: Option<bool>,
|
||||
|
||||
pub has_downloads: Option<bool>,
|
||||
|
||||
pub archived: Option<bool>,
|
||||
|
||||
pub disabled: Option<bool>,
|
||||
|
||||
pub visibility: Option<String>,
|
||||
pub pushed_at: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
|
||||
pub permissions: Option<serde_json::Value>,
|
||||
|
||||
pub allow_rebase_merge: Option<bool>,
|
||||
|
||||
pub template_repository: Option<Box<Repository>>,
|
||||
|
||||
pub allow_squash_merge: Option<bool>,
|
||||
|
||||
pub allow_merge_commit: Option<bool>,
|
||||
|
||||
pub allow_update_branch: Option<bool>,
|
||||
|
||||
pub allow_forking: Option<bool>,
|
||||
|
||||
pub subscribers_count: Option<i64>,
|
||||
|
||||
pub network_count: Option<i64>,
|
||||
|
||||
pub license: Option<serde_json::Value>,
|
||||
|
||||
pub allow_auto_merge: Option<bool>,
|
||||
|
||||
pub delete_branch_on_merge: Option<bool>,
|
||||
|
||||
pub parent: Option<Box<Repository>>,
|
||||
|
||||
pub source: Option<Box<Repository>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct Subject {
|
||||
pub title: String,
|
||||
pub url: Option<String>,
|
||||
pub latest_comment_url: Option<String>,
|
||||
pub r#type: String,
|
||||
}
|
||||
|
||||
/// Get new notifications.
|
||||
/// Returns: notifications and new last-modified value.
|
||||
pub fn get_new_notifications(
|
||||
pat: &str,
|
||||
last_modified: Option<&str>,
|
||||
) -> Result<(Vec<Notification>, Option<String>), Box<dyn Error>> {
|
||||
let mut resp = ureq::get("https://api.github.com/notifications").set("Authorization", &format!("Bearer {pat}"));
|
||||
if let Some(val) = last_modified {
|
||||
resp = resp.set("If-Modified-Since", val);
|
||||
}
|
||||
let json = resp.call()?.into_string()?;
|
||||
let items: Vec<Notification> = serde_json::from_str(&json)?;
|
||||
let last_modified = items
|
||||
.get(0)
|
||||
.map(|x| x.updated_at.clone())
|
||||
.or_else(|| last_modified.map(|x| x.to_owned()));
|
||||
Ok((items, last_modified))
|
||||
}
|
@ -17,6 +17,8 @@ use gpiocdev::{
|
||||
#[cfg(feature = "pc")]
|
||||
use image::{ImageBuffer, Rgb};
|
||||
|
||||
pub mod github;
|
||||
|
||||
#[cfg(feature = "pc")]
|
||||
pub struct FrameOutput {
|
||||
pub buffer: ImageBuffer<Rgb<u8>, Vec<u8>>,
|
||||
@ -43,7 +45,8 @@ impl DrawTarget for FrameOutput {
|
||||
{
|
||||
for pos in pixels {
|
||||
if pos.0.x < 0
|
||||
|| pos.0.y < 0 || pos.0.x as u32 >= self.buffer.width()
|
||||
|| pos.0.y < 0
|
||||
|| pos.0.x as u32 >= self.buffer.width()
|
||||
|| pos.0.y as u32 >= self.buffer.height()
|
||||
{
|
||||
continue;
|
||||
|
Loading…
Reference in New Issue
Block a user