Add LICENSE and update README for new repository
- Add MIT LICENSE file - Update README.md to reflect new API integration - Include all recent changes to migrate from PocketBase to church API 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fdfd83c747
commit
d911689e3a
2162
Cargo.lock
generated
2162
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -21,8 +21,8 @@ announcements, and information in a beautiful and engaging way.
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.36", features = ["full"] }
|
tokio = { version = "1.36", features = ["full"] }
|
||||||
iced = { git = "https://github.com/iced-rs/iced.git", features = ["image", "tokio", "advanced", "debug", "system"] }
|
iced = { version = "0.13", features = ["image", "tokio", "advanced", "debug", "system"] }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
@ -36,5 +36,6 @@ toml = "0.8"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
ril = { version = "0.10", features = ["all"] }
|
ril = { version = "0.10", features = ["all"] }
|
||||||
infer = "0.15"
|
infer = "0.15"
|
||||||
|
church-core = { path = "../church-core" }
|
||||||
[package.metadata.iced.assets]
|
[package.metadata.iced.assets]
|
||||||
icon = "icons/appicon.png"
|
icon = "icons/appicon.png"
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Rockville Tolland Seventh-day Adventist Church
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -14,18 +14,18 @@ A modern digital signage application for displaying church events, built with Ru
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Rust 1.70 or higher
|
- Rust 1.70 or higher
|
||||||
- A running Pocketbase instance with events collection
|
- Access to the church API for events data
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Create a `config.toml` file in the application directory with the following settings:
|
Create a `config.toml` file in the application directory with the following settings:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
pocketbase_url = "http://your-pocketbase-url"
|
api_url = "https://api.rockvilletollandsda.church/api"
|
||||||
window_width = 1920
|
window_width = 1920
|
||||||
window_height = 1080
|
window_height = 1080
|
||||||
slide_interval_secs = 10
|
slide_interval_seconds = 10
|
||||||
refresh_interval_mins = 5
|
refresh_interval_minutes = 5
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
|
@ -40,7 +40,7 @@ impl Settings {
|
||||||
impl Default for Settings {
|
impl Default for Settings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
api_url: String::from("https://api.rockvilletollandsda.church"),
|
api_url: String::from("https://api.rockvilletollandsda.church/api"),
|
||||||
window_width: 1920,
|
window_width: 1920,
|
||||||
window_height: 1080,
|
window_height: 1080,
|
||||||
slide_interval_seconds: 10,
|
slide_interval_seconds: 10,
|
||||||
|
|
237
src/main.rs
237
src/main.rs
|
@ -1,18 +1,16 @@
|
||||||
mod config;
|
mod config;
|
||||||
mod pocketbase;
|
|
||||||
|
|
||||||
use crate::pocketbase::ApiEvent;
|
use church_core::models::event::Event;
|
||||||
|
use church_core::client::ChurchApiClient;
|
||||||
|
use church_core::config::ChurchCoreConfig;
|
||||||
use iced::widget::{column, row, image, container, text};
|
use iced::widget::{column, row, image, container, text};
|
||||||
use iced::{
|
use iced::{
|
||||||
window, Element,
|
window, Element,
|
||||||
Length, Settings, Subscription, Theme, Task,
|
Length, Settings, Subscription, Theme, Task,
|
||||||
};
|
};
|
||||||
use iced::executor;
|
|
||||||
pub use iced::Program as IcedProgram;
|
|
||||||
use iced::Color;
|
use iced::Color;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use iced::window::settings::PlatformSpecific;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,8 +22,10 @@ static SETTINGS: Lazy<config::Settings> = Lazy::new(|| {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
static API_CLIENT: Lazy<pocketbase::ApiClient> = Lazy::new(|| {
|
static API_CLIENT: Lazy<ChurchApiClient> = Lazy::new(|| {
|
||||||
pocketbase::ApiClient::new(SETTINGS.api_url.clone())
|
let config = ChurchCoreConfig::new()
|
||||||
|
.with_base_url(SETTINGS.api_url.clone());
|
||||||
|
ChurchApiClient::new(config).expect("Failed to create API client")
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define some constants for styling
|
// Define some constants for styling
|
||||||
|
@ -54,20 +54,6 @@ struct DigitalSign {
|
||||||
is_fetching: bool,
|
is_fetching: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct Event {
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
start_time: String,
|
|
||||||
end_time: String,
|
|
||||||
date: String,
|
|
||||||
location: String,
|
|
||||||
//location_url: Option<String>,
|
|
||||||
image_url: Option<String>,
|
|
||||||
category: String,
|
|
||||||
//is_featured: bool,
|
|
||||||
timestamp: chrono::DateTime<chrono::Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
|
@ -77,43 +63,44 @@ enum Message {
|
||||||
ImageLoaded(String, image::Handle),
|
ImageLoaded(String, image::Handle),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IcedProgram for DigitalSign {
|
impl DigitalSign {
|
||||||
type Message = Message;
|
fn new() -> (Self, Task<Message>) {
|
||||||
type Theme = Theme;
|
let mut app = Self::default();
|
||||||
type Executor = executor::Default;
|
app.is_fetching = true;
|
||||||
type State = Self;
|
let task = Task::perform(fetch_events(), Message::handle_result);
|
||||||
type Renderer = iced::Renderer;
|
(app, task)
|
||||||
|
}
|
||||||
|
|
||||||
fn title(&self, _state: &Self::State, _window_id: window::Id) -> String {
|
fn title(&self) -> String {
|
||||||
String::from("Beacon")
|
String::from("Beacon")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&self, state: &mut Self::State, message: Message) -> Task<Message> {
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
Message::Tick => {
|
Message::Tick => {
|
||||||
let mut tasks = vec![];
|
let mut tasks = vec![];
|
||||||
state.loading_frame = (state.loading_frame + 1) % LOADING_FRAMES.len();
|
self.loading_frame = (self.loading_frame + 1) % LOADING_FRAMES.len();
|
||||||
|
|
||||||
if state.should_refresh() && !state.is_fetching {
|
if self.should_refresh() && !self.is_fetching {
|
||||||
tracing::info!("Refresh needed, starting event fetch");
|
tracing::info!("Refresh needed, starting event fetch");
|
||||||
state.is_fetching = true;
|
self.is_fetching = true;
|
||||||
tasks.push(Task::perform(fetch_events(), Message::handle_result));
|
tasks.push(Task::perform(fetch_events(), Message::handle_result));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !state.events.is_empty()
|
if !self.events.is_empty()
|
||||||
&& Instant::now().duration_since(state.last_update) >= SETTINGS.slide_interval()
|
&& Instant::now().duration_since(self.last_update) >= SETTINGS.slide_interval()
|
||||||
{
|
{
|
||||||
let next_index = (state.current_event_index + 1) % state.events.len();
|
let next_index = (self.current_event_index + 1) % self.events.len();
|
||||||
tracing::info!("Updating current event index from {} to {}",
|
tracing::info!("Updating current event index from {} to {}",
|
||||||
state.current_event_index,
|
self.current_event_index,
|
||||||
next_index
|
next_index
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear all images that aren't needed anymore
|
// Clear all images that aren't needed anymore
|
||||||
let mut urls_to_remove = Vec::new();
|
let mut urls_to_remove = Vec::new();
|
||||||
for url in state.loaded_images.keys() {
|
for url in self.loaded_images.keys() {
|
||||||
let is_needed = state.events.iter().any(|e| {
|
let is_needed = self.events.iter().any(|e| {
|
||||||
e.image_url.as_ref().map_or(false, |event_url| event_url == url)
|
e.image.as_ref().map_or(false, |event_url| event_url == url)
|
||||||
});
|
});
|
||||||
if !is_needed {
|
if !is_needed {
|
||||||
urls_to_remove.push(url.clone());
|
urls_to_remove.push(url.clone());
|
||||||
|
@ -121,17 +108,17 @@ impl IcedProgram for DigitalSign {
|
||||||
}
|
}
|
||||||
for url in urls_to_remove {
|
for url in urls_to_remove {
|
||||||
tracing::info!("Removing unused image: {}", url);
|
tracing::info!("Removing unused image: {}", url);
|
||||||
state.loaded_images.remove(&url);
|
self.loaded_images.remove(&url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current index and load new image if needed
|
// Update current index and load new image if needed
|
||||||
state.current_event_index = next_index;
|
self.current_event_index = next_index;
|
||||||
state.last_update = Instant::now();
|
self.last_update = Instant::now();
|
||||||
|
|
||||||
if let Some(current_event) = state.events.get(state.current_event_index) {
|
if let Some(current_event) = self.events.get(self.current_event_index) {
|
||||||
if let Some(url) = ¤t_event.image_url {
|
if let Some(url) = ¤t_event.image {
|
||||||
let url_clone = url.clone();
|
let url_clone = url.clone();
|
||||||
if !state.loaded_images.contains_key(&url_clone) {
|
if !self.loaded_images.contains_key(&url_clone) {
|
||||||
tracing::info!("Starting image load for new current event: {}", url_clone);
|
tracing::info!("Starting image load for new current event: {}", url_clone);
|
||||||
let url_for_closure = url_clone.clone();
|
let url_for_closure = url_clone.clone();
|
||||||
tasks.push(Task::perform(
|
tasks.push(Task::perform(
|
||||||
|
@ -156,26 +143,26 @@ impl IcedProgram for DigitalSign {
|
||||||
tracing::info!("Events loaded: {} events", events.len());
|
tracing::info!("Events loaded: {} events", events.len());
|
||||||
|
|
||||||
// Clear all existing images as we have a new set of events
|
// Clear all existing images as we have a new set of events
|
||||||
state.loaded_images.clear();
|
self.loaded_images.clear();
|
||||||
tracing::info!("Cleared all existing images");
|
tracing::info!("Cleared all existing images");
|
||||||
|
|
||||||
state.events = events;
|
self.events = events;
|
||||||
|
|
||||||
// Reset current event index if needed
|
// Reset current event index if needed
|
||||||
if state.current_event_index >= state.events.len() && !state.events.is_empty() {
|
if self.current_event_index >= self.events.len() && !self.events.is_empty() {
|
||||||
tracing::info!("Resetting current event index from {} to 0", state.current_event_index);
|
tracing::info!("Resetting current event index from {} to 0", self.current_event_index);
|
||||||
state.current_event_index = 0;
|
self.current_event_index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.last_refresh = Instant::now();
|
self.last_refresh = Instant::now();
|
||||||
state.is_fetching = false;
|
self.is_fetching = false;
|
||||||
|
|
||||||
// Load all images in parallel
|
// Load all images in parallel
|
||||||
let mut image_tasks = Vec::new();
|
let mut image_tasks = Vec::new();
|
||||||
|
|
||||||
// First, add the current event's image if it exists
|
// First, add the current event's image if it exists
|
||||||
if let Some(event) = state.events.get(state.current_event_index) {
|
if let Some(event) = self.events.get(self.current_event_index) {
|
||||||
if let Some(url) = &event.image_url {
|
if let Some(url) = &event.image {
|
||||||
tracing::info!("Starting immediate load for current image: {}", url);
|
tracing::info!("Starting immediate load for current image: {}", url);
|
||||||
let url_clone = url.clone();
|
let url_clone = url.clone();
|
||||||
image_tasks.push(Task::perform(
|
image_tasks.push(Task::perform(
|
||||||
|
@ -186,9 +173,9 @@ impl IcedProgram for DigitalSign {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then queue the rest of the images
|
// Then queue the rest of the images
|
||||||
for (index, event) in state.events.iter().enumerate() {
|
for (index, event) in self.events.iter().enumerate() {
|
||||||
if index != state.current_event_index {
|
if index != self.current_event_index {
|
||||||
if let Some(url) = &event.image_url {
|
if let Some(url) = &event.image {
|
||||||
tracing::info!("Queueing image preload for: {}", url);
|
tracing::info!("Queueing image preload for: {}", url);
|
||||||
let url_clone = url.clone();
|
let url_clone = url.clone();
|
||||||
image_tasks.push(Task::perform(
|
image_tasks.push(Task::perform(
|
||||||
|
@ -208,23 +195,25 @@ impl IcedProgram for DigitalSign {
|
||||||
}
|
}
|
||||||
Message::ImageLoaded(url, handle) => {
|
Message::ImageLoaded(url, handle) => {
|
||||||
tracing::info!("Image loaded: {}", url);
|
tracing::info!("Image loaded: {}", url);
|
||||||
state.loaded_images.insert(url, handle);
|
self.loaded_images.insert(url, handle);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::Error(error) => {
|
Message::Error(error) => {
|
||||||
tracing::error!("Error: {}", error);
|
tracing::error!("Error: {}", error);
|
||||||
state.is_fetching = false;
|
self.is_fetching = false;
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view<'a>(
|
fn view(&self) -> Element<Message> {
|
||||||
&self,
|
let content: Element<Message> = if let Some(event) = self.events.get(self.current_event_index) {
|
||||||
state: &'a Self::State,
|
let formatted_date = event.formatted_date();
|
||||||
_window_id: window::Id,
|
let formatted_start_time = event.formatted_start_time();
|
||||||
) -> Element<'a, Message, Theme, Self::Renderer> {
|
let formatted_end_time = event.formatted_end_time();
|
||||||
let content: Element<'a, Message, Theme, Self::Renderer> = if let Some(event) = state.events.get(state.current_event_index) {
|
let clean_description = event.clean_description();
|
||||||
|
let category_string = event.category.to_string();
|
||||||
|
|
||||||
let mut main_column = column![].spacing(40).padding(60).width(Length::Fill);
|
let mut main_column = column![].spacing(40).padding(60).width(Length::Fill);
|
||||||
|
|
||||||
// Left column with title and image
|
// Left column with title and image
|
||||||
|
@ -240,8 +229,8 @@ impl IcedProgram for DigitalSign {
|
||||||
|
|
||||||
// Image container with enhanced styling
|
// Image container with enhanced styling
|
||||||
container(
|
container(
|
||||||
if let Some(ref image_url) = event.image_url {
|
if let Some(ref image_url) = event.image {
|
||||||
if let Some(handle) = state.loaded_images.get(image_url) {
|
if let Some(handle) = self.loaded_images.get(image_url) {
|
||||||
container(
|
container(
|
||||||
image::Image::new(handle.clone())
|
image::Image::new(handle.clone())
|
||||||
.width(Length::Fixed(900.0))
|
.width(Length::Fixed(900.0))
|
||||||
|
@ -254,7 +243,7 @@ impl IcedProgram for DigitalSign {
|
||||||
} else {
|
} else {
|
||||||
container(
|
container(
|
||||||
column![
|
column![
|
||||||
text(LOADING_FRAMES[state.loading_frame])
|
text(LOADING_FRAMES[self.loading_frame])
|
||||||
.size(80)
|
.size(80)
|
||||||
.style(|_: &Theme| text::Style { color: Some(ACCENT_COLOR), ..Default::default() }),
|
.style(|_: &Theme| text::Style { color: Some(ACCENT_COLOR), ..Default::default() }),
|
||||||
text("Loading image...")
|
text("Loading image...")
|
||||||
|
@ -286,7 +275,7 @@ impl IcedProgram for DigitalSign {
|
||||||
let right_column = column![
|
let right_column = column![
|
||||||
// Category badge with gradient-like effect
|
// Category badge with gradient-like effect
|
||||||
container(
|
container(
|
||||||
text(event.category.to_uppercase())
|
text(category_string.to_uppercase())
|
||||||
.size(36)
|
.size(36)
|
||||||
.style(|_: &Theme| text::Style { color: Some(TEXT_COLOR), ..Default::default() })
|
.style(|_: &Theme| text::Style { color: Some(TEXT_COLOR), ..Default::default() })
|
||||||
)
|
)
|
||||||
|
@ -299,10 +288,10 @@ impl IcedProgram for DigitalSign {
|
||||||
// Date and time with enhanced colors
|
// Date and time with enhanced colors
|
||||||
container(
|
container(
|
||||||
column![
|
column![
|
||||||
text(&event.date)
|
text(formatted_date)
|
||||||
.size(64)
|
.size(64)
|
||||||
.style(|_: &Theme| text::Style { color: Some(DATE_COLOR), ..Default::default() }),
|
.style(|_: &Theme| text::Style { color: Some(DATE_COLOR), ..Default::default() }),
|
||||||
text(format!("{} - {}", event.start_time, event.end_time))
|
text(format!("{} - {}", formatted_start_time, formatted_end_time))
|
||||||
.size(56)
|
.size(56)
|
||||||
.style(|_: &Theme| text::Style { color: Some(TIME_COLOR), ..Default::default() })
|
.style(|_: &Theme| text::Style { color: Some(TIME_COLOR), ..Default::default() })
|
||||||
]
|
]
|
||||||
|
@ -332,7 +321,7 @@ impl IcedProgram for DigitalSign {
|
||||||
|
|
||||||
// Description with styled background
|
// Description with styled background
|
||||||
container(
|
container(
|
||||||
text(&event.description)
|
text(clean_description)
|
||||||
.size(44)
|
.size(44)
|
||||||
.style(|_: &Theme| text::Style { color: Some(TEXT_COLOR), ..Default::default() })
|
.style(|_: &Theme| text::Style { color: Some(TEXT_COLOR), ..Default::default() })
|
||||||
)
|
)
|
||||||
|
@ -386,12 +375,12 @@ impl IcedProgram for DigitalSign {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self, _state: &Self::State) -> Subscription<Message> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
iced::time::every(std::time::Duration::from_millis(100))
|
iced::time::every(std::time::Duration::from_millis(100))
|
||||||
.map(|_| Message::Tick)
|
.map(|_| Message::Tick)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn theme(&self, _state: &Self::State, _window_id: window::Id) -> Theme {
|
fn theme(&self) -> Theme {
|
||||||
Theme::Dark
|
Theme::Dark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -407,36 +396,30 @@ impl Message {
|
||||||
|
|
||||||
async fn fetch_events() -> Result<Vec<Event>, anyhow::Error> {
|
async fn fetch_events() -> Result<Vec<Event>, anyhow::Error> {
|
||||||
tracing::info!("Starting to fetch upcoming events from API");
|
tracing::info!("Starting to fetch upcoming events from API");
|
||||||
let api_events = match API_CLIENT.fetch_events().await {
|
let mut events = match API_CLIENT.get_upcoming_events(None).await {
|
||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
tracing::info!("Successfully fetched {} upcoming events from API", events.len());
|
tracing::info!("Successfully fetched {} events from API", events.len());
|
||||||
events
|
events
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to fetch events from API: {}", e);
|
tracing::error!("Failed to fetch events from API: {}", e);
|
||||||
return Err(e);
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert API events to display events (no filtering needed since /upcoming endpoint handles it)
|
|
||||||
let mut events: Vec<Event> = api_events
|
|
||||||
.into_iter()
|
|
||||||
.map(Event::from)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if events.is_empty() {
|
if events.is_empty() {
|
||||||
tracing::warn!("No upcoming events found");
|
tracing::warn!("No upcoming events found");
|
||||||
} else {
|
} else {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Found {} upcoming events, from {} to {}",
|
"Found {} upcoming events, from {} to {}",
|
||||||
events.len(),
|
events.len(),
|
||||||
events.first().map(|e| e.date.as_str()).unwrap_or("unknown"),
|
events.first().map(|e| e.formatted_date()).unwrap_or("unknown".to_string()),
|
||||||
events.last().map(|e| e.date.as_str()).unwrap_or("unknown")
|
events.last().map(|e| e.formatted_date()).unwrap_or("unknown".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by start time (API should already provide them sorted, but ensure consistency)
|
// Sort by start time
|
||||||
events.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
events.sort_by(|a, b| a.start_time.cmp(&b.start_time));
|
||||||
tracing::info!("Processed {} upcoming events", events.len());
|
tracing::info!("Processed {} upcoming events", events.len());
|
||||||
Ok(events)
|
Ok(events)
|
||||||
}
|
}
|
||||||
|
@ -488,38 +471,6 @@ async fn load_image(url: String) -> image::Handle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ApiEvent> for Event {
|
|
||||||
fn from(event: ApiEvent) -> Self {
|
|
||||||
let clean_description = html2text::from_read(event.description.as_bytes(), 80)
|
|
||||||
.replace('\n', " ")
|
|
||||||
.split_whitespace()
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
let date = event.start_time.format("%A, %B %d, %Y").to_string();
|
|
||||||
let start_time = event.start_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string();
|
|
||||||
let end_time = event.end_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string();
|
|
||||||
|
|
||||||
let image_url = event.image.clone();
|
|
||||||
if let Some(ref url) = image_url {
|
|
||||||
tracing::info!("Using image URL: {}", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
title: event.title,
|
|
||||||
description: clean_description,
|
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
date,
|
|
||||||
location: event.location,
|
|
||||||
//location_url: event.location_url,
|
|
||||||
image_url,
|
|
||||||
category: event.category,
|
|
||||||
//is_featured: event.is_featured,
|
|
||||||
timestamp: event.start_time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
|
@ -574,23 +525,6 @@ fn main() -> iced::Result {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let window_settings = window::Settings {
|
|
||||||
size: iced::Size::new(SETTINGS.window_width as f32, SETTINGS.window_height as f32),
|
|
||||||
position: window::Position::Centered,
|
|
||||||
fullscreen: true,
|
|
||||||
resizable: false,
|
|
||||||
decorations: false,
|
|
||||||
icon: icon_data,
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
platform_specific: PlatformSpecific {
|
|
||||||
title_hidden: true,
|
|
||||||
titlebar_transparent: true,
|
|
||||||
fullsize_content_view: true,
|
|
||||||
},
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
platform_specific: PlatformSpecific::default(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load additional fonts for better Unicode support
|
// Load additional fonts for better Unicode support
|
||||||
let fonts = vec![
|
let fonts = vec![
|
||||||
|
@ -605,34 +539,17 @@ fn main() -> iced::Result {
|
||||||
].into_iter().filter_map(|f| f).map(|bytes| std::borrow::Cow::Owned(bytes)).collect();
|
].into_iter().filter_map(|f| f).map(|bytes| std::borrow::Cow::Owned(bytes)).collect();
|
||||||
|
|
||||||
let settings = Settings {
|
let settings = Settings {
|
||||||
// window: window_settings,
|
|
||||||
//flags: (),
|
|
||||||
fonts,
|
fonts,
|
||||||
default_font: iced::Font::default(),
|
default_text_size: iced::Pixels(16.0),
|
||||||
antialiasing: true,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the initial state and start loading events
|
iced::application("Beacon", DigitalSign::update, DigitalSign::view)
|
||||||
let mut app = DigitalSign::default();
|
.subscription(DigitalSign::subscription)
|
||||||
app.is_fetching = true;
|
.theme(DigitalSign::theme)
|
||||||
|
.settings(settings)
|
||||||
DigitalSign::run_with(
|
.window_size(iced::Size::new(800.0, 600.0))
|
||||||
app,
|
.run_with(DigitalSign::new)
|
||||||
settings,
|
|
||||||
Some(window_settings),
|
|
||||||
|| {
|
|
||||||
let mut state = DigitalSign::default();
|
|
||||||
state.is_fetching = true;
|
|
||||||
(
|
|
||||||
state,
|
|
||||||
Task::perform(
|
|
||||||
fetch_events(),
|
|
||||||
Message::handle_result
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DigitalSign {
|
impl DigitalSign {
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
const API_TIMEOUT: Duration = Duration::from_secs(10);
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct ApiEvent {
|
|
||||||
pub id: String,
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub start_time: DateTime<Utc>,
|
|
||||||
pub end_time: DateTime<Utc>,
|
|
||||||
pub location: String,
|
|
||||||
pub location_url: Option<String>,
|
|
||||||
pub image: Option<String>,
|
|
||||||
pub thumbnail: Option<String>,
|
|
||||||
pub category: String,
|
|
||||||
pub is_featured: bool,
|
|
||||||
pub recurring_type: Option<String>,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ApiClient {
|
|
||||||
client: reqwest::Client,
|
|
||||||
base_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiClient {
|
|
||||||
pub fn new(base_url: String) -> Self {
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(API_TIMEOUT)
|
|
||||||
.build()
|
|
||||||
.expect("Failed to create HTTP client");
|
|
||||||
|
|
||||||
Self { client, base_url }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_events(&self) -> Result<Vec<ApiEvent>> {
|
|
||||||
let url = format!("{}/api/events/upcoming", self.base_url);
|
|
||||||
tracing::info!("Fetching events from URL: {}", url);
|
|
||||||
|
|
||||||
let response = match self.client.get(&url)
|
|
||||||
.header("Cache-Control", "max-age=60") // Cache for 60 seconds
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(resp) => {
|
|
||||||
tracing::info!("Got response with status: {}", resp.status());
|
|
||||||
resp
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("HTTP request failed: {}", e);
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = match response.error_for_status() {
|
|
||||||
Ok(resp) => resp,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("HTTP error status: {}", e);
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct ApiResponse {
|
|
||||||
success: bool,
|
|
||||||
data: Vec<ApiEvent>,
|
|
||||||
}
|
|
||||||
|
|
||||||
match response.json().await {
|
|
||||||
Ok(api_response) => {
|
|
||||||
let ApiResponse { success, data } = api_response;
|
|
||||||
if success {
|
|
||||||
tracing::info!("Successfully parsed {} events from response", data.len());
|
|
||||||
Ok(data)
|
|
||||||
} else {
|
|
||||||
tracing::error!("API returned success: false");
|
|
||||||
Err(anyhow::anyhow!("API request failed"))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to parse JSON response: {}", e);
|
|
||||||
Err(e.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue