Compare commits
6 Commits
fix/transp
...
4b33ac4bde
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b33ac4bde | |||
| ced41148ef | |||
| 11adc10c34 | |||
| c5ae7cdd96 | |||
| d737322fa6 | |||
| 2e50370e2c |
21
.env.example
21
.env.example
@@ -1,20 +1,25 @@
|
|||||||
# Flask Configuration
|
# Flask Configuration
|
||||||
FLASK_SECRET_KEY=your_secret_key_here
|
FLASK_SECRET_KEY=calendar-display-secret-key-change-this
|
||||||
|
|
||||||
# OpenWeatherMap API
|
# OpenWeatherMap API
|
||||||
OPENWEATHER_API_KEY=your_api_key_here
|
# Get your free API key from: https://openweathermap.org/api
|
||||||
|
OPENWEATHER_API_KEY=your_openweather_api_key_here
|
||||||
|
|
||||||
# Weather Location (Hamilton, New Zealand)
|
# Weather Location
|
||||||
WEATHER_LOCATION=Hamilton,NZ
|
# Find coordinates at: https://www.latlong.net/
|
||||||
WEATHER_LAT=-37.7870
|
WEATHER_LOCATION=YourCity,CountryCode
|
||||||
WEATHER_LON=175.2793
|
WEATHER_LAT=0.0000
|
||||||
|
WEATHER_LON=0.0000
|
||||||
|
|
||||||
# Google Calendar
|
# Google Calendar
|
||||||
|
# Option 1: Use public iCal URL (easiest - no authentication needed)
|
||||||
|
# Get from: Google Calendar Settings > Calendar > Integrate Calendar > Public URL to this calendar (iCal format)
|
||||||
GOOGLE_CALENDAR_ID=your_calendar_id@group.calendar.google.com
|
GOOGLE_CALENDAR_ID=your_calendar_id@group.calendar.google.com
|
||||||
|
GOOGLE_CALENDAR_ICAL_URL=https://calendar.google.com/calendar/ical/your_calendar_id%40group.calendar.google.com/public/basic.ics
|
||||||
|
|
||||||
# Update Intervals (seconds)
|
# Update Intervals (seconds)
|
||||||
IMAGE_ROTATION_INTERVAL=300
|
IMAGE_ROTATION_INTERVAL=60
|
||||||
WEATHER_UPDATE_INTERVAL=900
|
WEATHER_UPDATE_INTERVAL=900
|
||||||
CALENDAR_UPDATE_INTERVAL=300
|
CALENDAR_UPDATE_INTERVAL=300
|
||||||
JOKE_UPDATE_INTERVAL=3600
|
JOKE_UPDATE_INTERVAL=3600
|
||||||
CALENDAR_DAYS_AHEAD=7
|
CALENDAR_DAYS_AHEAD=5
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -43,3 +43,8 @@ Thumbs.db
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Background Images (users should add their own)
|
||||||
|
static/backgrounds/*
|
||||||
|
!static/backgrounds/README.md
|
||||||
|
!static/backgrounds/.gitkeep
|
||||||
|
|||||||
122
README.md
122
README.md
@@ -7,9 +7,10 @@ A smart display application for Raspberry Pi that shows time, weather, calendar
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🕐 **Real-time Clock** - Current time and date display
|
- 🕐 **Real-time Clock** - Current time and date display
|
||||||
- 🌤️ **Weather** - Current weather and 3-day forecast for Hamilton, New Zealand (OpenWeatherMap)
|
- 🌤️ **Weather** - Current weather and 3-day forecast (OpenWeatherMap)
|
||||||
- 📅 **Google Calendar** - Family calendar events display
|
- 📅 **Google Calendar** - Calendar events display (supports public iCal feeds)
|
||||||
- 🖼️ **Rotating Backgrounds** - Beautiful images from local directory
|
- 🖼️ **Rotating Backgrounds** - Beautiful images from local directory
|
||||||
|
- 🎨 **Dynamic Text Color** - Automatically adjusts text color based on background brightness
|
||||||
- 😄 **Dad Jokes** - Random jokes to brighten your day (optional)
|
- 😄 **Dad Jokes** - Random jokes to brighten your day (optional)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -55,26 +56,80 @@ GOOGLE_CALENDAR_ID=your_calendar_id@group.calendar.google.com
|
|||||||
FLASK_SECRET_KEY=your_random_secret_key
|
FLASK_SECRET_KEY=your_random_secret_key
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Set Up Google Calendar API
|
### 4. Get OpenWeatherMap API Key
|
||||||
|
|
||||||
|
1. Go to [OpenWeatherMap](https://openweathermap.org/api)
|
||||||
|
2. Sign up for a free account
|
||||||
|
3. Navigate to "API keys" section
|
||||||
|
4. Copy your API key
|
||||||
|
5. Add it to your `.env` file as `OPENWEATHER_API_KEY`
|
||||||
|
|
||||||
|
**Find Your Location Coordinates:**
|
||||||
|
1. Go to [LatLong.net](https://www.latlong.net/)
|
||||||
|
2. Search for your city
|
||||||
|
3. Copy the latitude and longitude values
|
||||||
|
4. Add them to your `.env` file as `WEATHER_LAT` and `WEATHER_LON`
|
||||||
|
|
||||||
|
### 5. Set Up Google Calendar
|
||||||
|
|
||||||
|
**Option 1: Public iCal Feed (Easiest - No Authentication)**
|
||||||
|
|
||||||
|
1. Open [Google Calendar](https://calendar.google.com/)
|
||||||
|
2. Go to Settings (⚙️ gear icon)
|
||||||
|
3. Select the calendar you want to display
|
||||||
|
4. Scroll down to "Integrate calendar"
|
||||||
|
5. Copy the **"Public URL to this calendar"** in iCal format
|
||||||
|
- The URL looks like: `https://calendar.google.com/calendar/ical/...@group.calendar.google.com/public/basic.ics`
|
||||||
|
6. Add this URL to your `.env` file as `GOOGLE_CALENDAR_ICAL_URL`
|
||||||
|
7. Also copy the Calendar ID (looks like `abc123...@group.calendar.google.com`)
|
||||||
|
8. Add it to your `.env` file as `GOOGLE_CALENDAR_ID`
|
||||||
|
|
||||||
|
**Note:** Your calendar must be set to "Public" for this method to work.
|
||||||
|
|
||||||
|
**Option 2: Google Calendar API with Credentials (Advanced)**
|
||||||
|
|
||||||
|
If you prefer to use private calendars with authentication:
|
||||||
|
|
||||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
2. Create a new project or select existing one
|
2. Create a new project or select existing one
|
||||||
3. Enable the Google Calendar API
|
3. Enable the Google Calendar API
|
||||||
4. Create credentials (OAuth 2.0 or Service Account)
|
4. Create credentials (Service Account)
|
||||||
5. Download the credentials JSON file
|
5. Download the credentials JSON file
|
||||||
6. Save it as [credentials/google_calendar_credentials.json](credentials/google_calendar_credentials.json)
|
6. Save it as `credentials/google_calendar_credentials.json`
|
||||||
|
7. Share your calendar with the service account email
|
||||||
|
|
||||||
### 5. Add Background Images
|
### 6. Add Background Images
|
||||||
|
|
||||||
Place your images in the [static/backgrounds/](static/backgrounds/) directory:
|
Place your images in the `static/backgrounds/` directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp /path/to/your/images/*.jpg static/backgrounds/
|
cp /path/to/your/images/*.jpg static/backgrounds/
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported formats: JPG, PNG, GIF, WebP
|
**Supported formats:** JPG, JPEG, PNG, GIF, WebP
|
||||||
|
|
||||||
### 6. Run the Application
|
**Recommended specifications:**
|
||||||
|
- Resolution: 1920x1080 or higher
|
||||||
|
- Aspect ratio: 16:9 (for full-screen displays)
|
||||||
|
- File size: Keep under 5MB for faster loading
|
||||||
|
|
||||||
|
**Changing Image Rotation Interval:**
|
||||||
|
|
||||||
|
Edit your `.env` file:
|
||||||
|
```bash
|
||||||
|
# Time in seconds (60 = 1 minute)
|
||||||
|
IMAGE_ROTATION_INTERVAL=60
|
||||||
|
```
|
||||||
|
|
||||||
|
Or edit `static/js/app.js`:
|
||||||
|
```javascript
|
||||||
|
const INTERVALS = {
|
||||||
|
BACKGROUND: 60000, // Milliseconds (60000 = 1 minute)
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Run the Application
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python app.py
|
python app.py
|
||||||
@@ -153,13 +208,50 @@ The display should now start automatically on boot!
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Edit [.env](.env) to customize:
|
### Environment Variables (`.env` file)
|
||||||
|
|
||||||
- `IMAGE_ROTATION_INTERVAL` - Seconds between background changes (default: 300)
|
**Weather Settings:**
|
||||||
- `WEATHER_UPDATE_INTERVAL` - Seconds between weather updates (default: 900)
|
- `OPENWEATHER_API_KEY` - Your OpenWeatherMap API key
|
||||||
- `CALENDAR_UPDATE_INTERVAL` - Seconds between calendar updates (default: 300)
|
- `WEATHER_LOCATION` - City name and country code (e.g., "London,UK")
|
||||||
- `JOKE_UPDATE_INTERVAL` - Seconds between joke updates (default: 3600)
|
- `WEATHER_LAT` - Latitude of your location
|
||||||
- `CALENDAR_DAYS_AHEAD` - Number of days ahead to show events (default: 7)
|
- `WEATHER_LON` - Longitude of your location
|
||||||
|
|
||||||
|
**Calendar Settings:**
|
||||||
|
- `GOOGLE_CALENDAR_ID` - Your calendar ID
|
||||||
|
- `GOOGLE_CALENDAR_ICAL_URL` - Public iCal feed URL (easiest method)
|
||||||
|
- `CALENDAR_DAYS_AHEAD` - Number of days ahead to show events (default: 5)
|
||||||
|
|
||||||
|
**Update Intervals (in seconds):**
|
||||||
|
- `IMAGE_ROTATION_INTERVAL` - Background image rotation (default: 60)
|
||||||
|
- `WEATHER_UPDATE_INTERVAL` - Weather refresh interval (default: 900)
|
||||||
|
- `CALENDAR_UPDATE_INTERVAL` - Calendar refresh interval (default: 300)
|
||||||
|
- `JOKE_UPDATE_INTERVAL` - Dad joke refresh interval (default: 3600)
|
||||||
|
|
||||||
|
**Other:**
|
||||||
|
- `FLASK_SECRET_KEY` - Flask session secret key
|
||||||
|
|
||||||
|
### JavaScript Configuration (`static/js/app.js`)
|
||||||
|
|
||||||
|
For more precise control over update intervals, edit the `INTERVALS` object:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const INTERVALS = {
|
||||||
|
TIME: 1000, // 1 second
|
||||||
|
WEATHER: 900000, // 15 minutes
|
||||||
|
CALENDAR: 300000, // 5 minutes
|
||||||
|
BACKGROUND: 60000, // 1 minute (change this for image rotation)
|
||||||
|
JOKE: 3600000 // 1 hour
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**To change image rotation time:**
|
||||||
|
1. Open `static/js/app.js`
|
||||||
|
2. Find the `INTERVALS` object at the top
|
||||||
|
3. Change `BACKGROUND: 60000` to your desired value in milliseconds
|
||||||
|
- 30000 = 30 seconds
|
||||||
|
- 60000 = 1 minute
|
||||||
|
- 120000 = 2 minutes
|
||||||
|
- 300000 = 5 minutes
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB |
48
static/backgrounds/README.md
Normal file
48
static/backgrounds/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Background Images
|
||||||
|
|
||||||
|
This directory is where you place your background images for the calendar display.
|
||||||
|
|
||||||
|
## Adding Images
|
||||||
|
|
||||||
|
1. Copy your image files to this directory:
|
||||||
|
```bash
|
||||||
|
cp /path/to/your/images/* static/backgrounds/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Supported formats:
|
||||||
|
- JPEG/JPG
|
||||||
|
- PNG
|
||||||
|
- GIF
|
||||||
|
- WebP
|
||||||
|
|
||||||
|
3. Recommended image specifications:
|
||||||
|
- Resolution: 1920x1080 or higher
|
||||||
|
- Aspect ratio: 16:9 (for full-screen displays)
|
||||||
|
- File size: Keep under 5MB for faster loading
|
||||||
|
|
||||||
|
## Image Rotation
|
||||||
|
|
||||||
|
Images automatically rotate at the interval specified in your `.env` file.
|
||||||
|
|
||||||
|
To change the rotation interval, edit the `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Time in seconds (default: 60 seconds = 1 minute)
|
||||||
|
IMAGE_ROTATION_INTERVAL=60
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly in `static/js/app.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const INTERVALS = {
|
||||||
|
BACKGROUND: 60000, // Milliseconds (60000 = 1 minute)
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Use high-quality images for best display
|
||||||
|
- The app will randomly select images from this directory
|
||||||
|
- Delete or move images you no longer want displayed
|
||||||
|
- Images are cached by the browser, so you may need to hard-refresh (Ctrl+F5) to see new images immediately
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 224 KiB |
@@ -11,6 +11,48 @@ body {
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
transition: color 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark text for bright backgrounds */
|
||||||
|
body.light-bg {
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-bg .time,
|
||||||
|
body.light-bg .date,
|
||||||
|
body.light-bg .current-temp,
|
||||||
|
body.light-bg .weather-label,
|
||||||
|
body.light-bg .weather-icon,
|
||||||
|
body.light-bg .forecast-icon,
|
||||||
|
body.light-bg section h2,
|
||||||
|
body.light-bg .day-name,
|
||||||
|
body.light-bg .day-date,
|
||||||
|
body.light-bg .event-title,
|
||||||
|
body.light-bg .event-location,
|
||||||
|
body.light-bg .joke {
|
||||||
|
text-shadow: 1px 1px 3px rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-bg .event-time {
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-bg .event {
|
||||||
|
border-left-color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* White text for dark backgrounds (default) */
|
||||||
|
body.dark-bg {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-bg .event-time {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-bg .event {
|
||||||
|
border-left-color: #4a9eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Background */
|
/* Background */
|
||||||
@@ -49,15 +91,13 @@ body {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header */
|
/* Header – maximally transparent; text readable via text-shadow */
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: rgba(0, 0, 0, 0.15);
|
background: rgba(0, 0, 0, 0.03);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
@@ -68,15 +108,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
font-size: 3rem;
|
font-size: 4.5rem;
|
||||||
font-weight: 300;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-size: 1.1rem;
|
font-size: 1.6rem;
|
||||||
font-weight: 300;
|
font-weight: 500;
|
||||||
margin-top: 0.3rem;
|
margin-top: 0.3rem;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
@@ -96,20 +136,21 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.weather-label {
|
.weather-label {
|
||||||
font-size: 0.85rem;
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-temp {
|
.current-temp {
|
||||||
font-size: 2rem;
|
font-size: 3rem;
|
||||||
font-weight: 300;
|
font-weight: 600;
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-icon {
|
.weather-icon {
|
||||||
font-size: 1.8rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-forecast {
|
.header-forecast {
|
||||||
@@ -125,17 +166,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.forecast-icon {
|
.forecast-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 2.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-temps {
|
.forecast-temps {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
font-size: 0.9rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-temps .high {
|
.forecast-temps .high {
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-temps .low {
|
.forecast-temps .low {
|
||||||
@@ -151,9 +192,7 @@ body {
|
|||||||
|
|
||||||
/* Sections */
|
/* Sections */
|
||||||
section {
|
section {
|
||||||
background: rgba(0, 0, 0, 0.15);
|
background: rgba(0, 0, 0, 0.03);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -161,8 +200,8 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
section h2 {
|
section h2 {
|
||||||
font-size: 1.3rem;
|
font-size: 2rem;
|
||||||
font-weight: 400;
|
font-weight: 700;
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.8rem;
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
@@ -212,7 +251,7 @@ section h3 {
|
|||||||
grid-template-columns: 60px 1fr 100px;
|
grid-template-columns: 60px 1fr 100px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.6rem;
|
padding: 0.6rem;
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
}
|
}
|
||||||
@@ -261,20 +300,21 @@ section h3 {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.6rem;
|
padding: 0.6rem;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
border-radius: 8px 8px 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-name {
|
.day-name {
|
||||||
font-size: 1rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-date {
|
.day-date {
|
||||||
font-size: 0.85rem;
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
}
|
}
|
||||||
@@ -288,7 +328,8 @@ section h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.no-events {
|
.no-events {
|
||||||
font-size: 0.85rem;
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
@@ -296,27 +337,29 @@ section h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.event {
|
.event {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
padding: 0.5rem 0.6rem;
|
padding: 0.5rem 0.6rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border-left: 3px solid #4a9eff;
|
border-left: 3px solid #4a9eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-time {
|
.event-time {
|
||||||
font-size: 0.75rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
color: #4a9eff;
|
color: #4a9eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-title {
|
.event-title {
|
||||||
font-size: 0.9rem;
|
font-size: 1.3rem;
|
||||||
|
font-weight: 600;
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-location {
|
.event-location {
|
||||||
font-size: 0.85rem;
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
@@ -331,15 +374,14 @@ section h3 {
|
|||||||
/* Footer */
|
/* Footer */
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
background: rgba(0, 0, 0, 0.15);
|
background: rgba(0, 0, 0, 0.03);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
padding: 0.8rem 1.5rem;
|
padding: 0.8rem 1.5rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.joke {
|
.joke {
|
||||||
font-size: 0.9rem;
|
font-size: 1.3rem;
|
||||||
|
font-weight: 500;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
@@ -368,19 +410,19 @@ section h3 {
|
|||||||
/* Responsive adjustments for smaller screens */
|
/* Responsive adjustments for smaller screens */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.time {
|
.time {
|
||||||
font-size: 4rem;
|
font-size: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-size: 1.5rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-temp {
|
.current-temp {
|
||||||
font-size: 3rem;
|
font-size: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
section h2 {
|
section h2 {
|
||||||
font-size: 1.6rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,10 +432,10 @@ section h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
font-size: 3rem;
|
font-size: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-temp {
|
.current-temp {
|
||||||
font-size: 2.5rem;
|
font-size: 3.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,56 @@ async function updateCalendar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate brightness of an image
|
||||||
|
function calculateImageBrightness(imageSrc, callback) {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'Anonymous';
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
// Create a canvas to analyze the image
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Use a smaller size for faster processing
|
||||||
|
canvas.width = 100;
|
||||||
|
canvas.height = 100;
|
||||||
|
|
||||||
|
// Draw the image scaled down
|
||||||
|
ctx.drawImage(img, 0, 0, 100, 100);
|
||||||
|
|
||||||
|
// Get image data
|
||||||
|
const imageData = ctx.getImageData(0, 0, 100, 100);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
// Calculate average brightness
|
||||||
|
let totalBrightness = 0;
|
||||||
|
const pixelCount = data.length / 4;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
const r = data[i];
|
||||||
|
const g = data[i + 1];
|
||||||
|
const b = data[i + 2];
|
||||||
|
|
||||||
|
// Calculate perceived brightness (using luminance formula)
|
||||||
|
const brightness = (0.299 * r + 0.587 * g + 0.114 * b);
|
||||||
|
totalBrightness += brightness;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avgBrightness = totalBrightness / pixelCount;
|
||||||
|
|
||||||
|
// Return brightness value (0-255)
|
||||||
|
callback(avgBrightness);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
console.error('Error loading image for brightness analysis');
|
||||||
|
// Default to dark background if error
|
||||||
|
callback(100);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = imageSrc;
|
||||||
|
}
|
||||||
|
|
||||||
// Update background image
|
// Update background image
|
||||||
async function updateBackground() {
|
async function updateBackground() {
|
||||||
try {
|
try {
|
||||||
@@ -226,11 +276,28 @@ async function updateBackground() {
|
|||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
backgroundElement.style.backgroundImage = `url('${data.image}')`;
|
backgroundElement.style.backgroundImage = `url('${data.image}')`;
|
||||||
|
|
||||||
|
// Analyze brightness and adjust text color
|
||||||
|
calculateImageBrightness(data.image, (brightness) => {
|
||||||
|
// Threshold: 128 is middle brightness
|
||||||
|
// If brightness > 140, use dark text (light background)
|
||||||
|
// If brightness <= 140, use white text (dark background)
|
||||||
|
if (brightness > 140) {
|
||||||
|
document.body.classList.remove('dark-bg');
|
||||||
|
document.body.classList.add('light-bg');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('light-bg');
|
||||||
|
document.body.classList.add('dark-bg');
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
img.src = data.image;
|
img.src = data.image;
|
||||||
} else if (data.color) {
|
} else if (data.color) {
|
||||||
backgroundElement.style.backgroundColor = data.color;
|
backgroundElement.style.backgroundColor = data.color;
|
||||||
backgroundElement.style.backgroundImage = 'none';
|
backgroundElement.style.backgroundImage = 'none';
|
||||||
|
// Assume solid colors are dark
|
||||||
|
document.body.classList.remove('light-bg');
|
||||||
|
document.body.classList.add('dark-bg');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user