Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -601,10 +601,10 @@
"plugin_path": "plugins/of-the-day",
"stars": 0,
"downloads": 0,
"last_updated": "2026-05-26",
"last_updated": "2026-05-30",
"verified": true,
"screenshot": "",
"latest_version": "1.2.0"
"latest_version": "1.3.0"
},
{
"id": "olympics",
Expand Down
86 changes: 70 additions & 16 deletions plugins/of-the-day/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"category_order",
"file_manager",
"categories",
"display_duration"
"display_duration",
"customization"
],
"properties": {
"enabled": {
Expand Down Expand Up @@ -40,7 +41,10 @@
"items": {
"type": "string"
},
"default": ["word_of_the_day", "slovenian_word_of_the_day"],
"default": [
"word_of_the_day",
"slovenian_word_of_the_day"
],
"description": "Order to display categories"
},
"categories": {
Expand All @@ -64,7 +68,11 @@
"description": "Display name shown in configuration"
}
},
"required": ["enabled", "data_file", "display_name"]
"required": [
"enabled",
"data_file",
"display_name"
]
}
},
"display_duration": {
Expand All @@ -80,35 +88,81 @@
"x-widget": "plugin-file-manager",
"x-widget-config": {
"actions": {
"list": "list-files",
"get": "get-file",
"save": "save-file",
"list": "list-files",
"get": "get-file",
"save": "save-file",
"upload": "upload-file",
"delete": "delete-file",
"create": "create-file",
"toggle": "toggle-category"
},
"upload_hint": "JSON files with day numbers 1–365 as keys",
"upload_hint": "JSON files with day numbers 1\u2013365 as keys",
"directory_label": "of_the_day/",
"create_fields": [
{
"key": "category_name",
"label": "Category Name",
"key": "category_name",
"label": "Category Name",
"placeholder": "e.g., my_words",
"pattern": "^[a-z0-9_]+$",
"hint": "Lowercase letters, numbers, underscores used as filename"
"pattern": "^[a-z0-9_]+$",
"hint": "Lowercase letters, numbers, underscores \u2014 used as filename"
},
{
"key": "display_name",
"label": "Display Name",
"key": "display_name",
"label": "Display Name",
"placeholder": "e.g., My Words",
"hint": "Shown in plugin configuration (leave blank to auto-generate)"
"hint": "Shown in plugin configuration (leave blank to auto-generate)"
}
],
"toggle_key": "category_name"
}
},
"customization": {
"type": "object",
"title": "Display Customization",
"description": "Customize fonts, sizes, colors and positioning for the display text. Requires a LEDMatrix core with the element-style system; on older cores this section is not shown and the classic styling is used.",
"x-style-elements": {
"title_text": {
"title": "Title",
"font": {
"default": "PressStart2P-Regular.ttf"
},
"size": {
"default": 8,
"min": 4,
"max": 16
},
"color": {
"default": [
255,
255,
255
]
},
"offsets": true
},
"body_text": {
"title": "Body Text",
"font": {
"default": "4x6-font.ttf"
},
"size": {
"default": 6,
"min": 4,
"max": 12
},
"color": {
"default": [
200,
200,
200
]
},
"offsets": true
}
}
}
},
"required": ["enabled"]
"required": [
"enabled"
]
}

147 changes: 94 additions & 53 deletions plugins/of-the-day/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@

from src.plugin_system.base_plugin import BasePlugin

# Shared element-style resolver (newer cores): user-customizable per-element
# fonts/sizes/colors/offsets, declared once in config_schema.json via
# x-style-elements. Older cores don't expand the declaration (no
# customization UI is shown) and this import guard keeps the classic
# styling path untouched there.
try:
from src.element_style import ElementStyleResolver, defaults_from_schema_file
STYLE_AVAILABLE = True
except ImportError:
STYLE_AVAILABLE = False

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -116,6 +127,50 @@ def _register_fonts(self):
self.logger.info("Of The Day fonts registered")
except Exception as e:
self.logger.warning(f"Error registering fonts: {e}")

def _element_styles(self):
"""Resolved (title, body) styles from customization config.

Returns (title_font, title_color, title_offset,
body_font, body_color, body_offset).

With an untouched config this resolves to exactly the classic fonts
and colors (PressStart2P@8 white / 4x6@6 gray), so rendering is
unchanged; a genuine user override in customization.title_text /
body_text wins. On older cores (no src.element_style) the classic
values are returned directly.
"""
if STYLE_AVAILABLE:
resolver = getattr(self, '_element_style_resolver', None)
if resolver is None or resolver._config is not self.config:
schema_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), 'config_schema.json')
resolver = ElementStyleResolver(
self.config, defaults_from_schema_file(schema_path))
self._element_style_resolver = resolver
title = resolver.style('title_text',
classic_font='PressStart2P-Regular.ttf',
classic_size=8,
classic_color=self.title_color)
body = resolver.style('body_text',
classic_font='4x6-font.ttf',
classic_size=6,
classic_color=self.subtitle_color)
return (title.font, title.color, title.offset,
body.font, body.color, body.offset)

try:
title_font = ImageFont.truetype('assets/fonts/PressStart2P-Regular.ttf', 8)
except Exception as e:
self.logger.warning(f"Failed to load PressStart2P font: {e}, using fallback")
title_font = self.display_manager.small_font if hasattr(self.display_manager, 'small_font') else ImageFont.load_default()
try:
body_font = ImageFont.truetype('assets/fonts/4x6-font.ttf', 6)
except Exception as e:
self.logger.warning(f"Failed to load 4x6 font: {e}, using fallback")
body_font = self.display_manager.extra_small_font if hasattr(self.display_manager, 'extra_small_font') else ImageFont.load_default()
return (title_font, self.title_color, (0, 0),
body_font, self.subtitle_color, (0, 0))

def _load_data_files(self):
"""Load all data files for enabled categories."""
Expand Down Expand Up @@ -400,23 +455,15 @@ def _display_title(self, category_config: Dict, item_data: Dict):
"""Display the title/word with subtitle, matching old manager layout."""
# Clear display first
self.display_manager.clear()

# Use display_manager's image and draw directly
draw = self.display_manager.draw

# Load fonts - match old manager font usage
try:
title_font = ImageFont.truetype('assets/fonts/PressStart2P-Regular.ttf', 8)
except Exception as e:
self.logger.warning(f"Failed to load PressStart2P font: {e}, using fallback")
title_font = self.display_manager.small_font if hasattr(self.display_manager, 'small_font') else ImageFont.load_default()

try:
body_font = ImageFont.truetype('assets/fonts/4x6-font.ttf', 6)
except Exception as e:
self.logger.warning(f"Failed to load 4x6 font: {e}, using fallback")
body_font = self.display_manager.extra_small_font if hasattr(self.display_manager, 'extra_small_font') else ImageFont.load_default()


# Fonts/colors/offsets honor customization.<element> (classic
# PressStart2P@8 / 4x6@6 when untouched)
(title_font, title_color, (title_dx, title_dy),
body_font, body_color, (body_dx, body_dy)) = self._element_styles()

# Get font heights
try:
title_height = self.display_manager.get_font_height(title_font)
Expand Down Expand Up @@ -451,30 +498,30 @@ def _display_title(self, category_config: Dict, item_data: Dict):
else:
title_width = len(title) * 6

# Center the title horizontally
title_x = (self.display_manager.width - title_width) // 2
title_y = margin_top
# Center the title horizontally (+ user layout offset)
title_x = (self.display_manager.width - title_width) // 2 + title_dx
title_y = margin_top + title_dy

# Draw title using display_manager.draw_text (proper method)
self.logger.info(f"Drawing title '{title}' at ({title_x}, {title_y}) with font type {type(title_font).__name__}")
try:
self.display_manager.draw_text(
title,
x=title_x,
y=title_y,
color=self.title_color,
color=title_color,
font=title_font
)
self.logger.debug(f"Title '{title}' drawn using display_manager.draw_text")
except Exception as e:
self.logger.error(f"Error drawing title '{title}': {e}", exc_info=True)

# Draw underline below title (like old manager)
underline_y = title_y + title_height + 1
underline_x_start = title_x
underline_x_end = title_x + title_width
draw.line([(underline_x_start, underline_y), (underline_x_end, underline_y)],
fill=self.title_color, width=1)
draw.line([(underline_x_start, underline_y), (underline_x_end, underline_y)],
fill=title_color, width=1)

# Draw subtitle below underline (centered, like old manager)
if subtitle:
Expand Down Expand Up @@ -503,39 +550,33 @@ def _display_title(self, category_config: Dict, item_data: Dict):
line_width = bbox[2] - bbox[0]
else:
line_width = len(line) * 6
line_x = (self.display_manager.width - line_width) // 2
line_x = (self.display_manager.width - line_width) // 2 + body_dx

# Use display_manager.draw_text for subtitle
self.display_manager.draw_text(
line,
x=line_x,
y=current_y,
color=self.subtitle_color,
y=current_y + body_dy,
color=body_color,
font=body_font
)
current_y += body_height + 1

self.display_manager.update_display()

def _display_content(self, category_config: Dict, item_data: Dict):
"""Display the definition/content, matching old manager layout."""
# Clear display first
self.display_manager.clear()

# Use display_manager's image and draw directly
draw = self.display_manager.draw

# Load fonts - match old manager
try:
title_font = ImageFont.truetype('assets/fonts/PressStart2P-Regular.ttf', 8)
except Exception:
title_font = self.display_manager.small_font if hasattr(self.display_manager, 'small_font') else ImageFont.load_default()

try:
body_font = ImageFont.truetype('assets/fonts/4x6-font.ttf', 6)
except Exception:
body_font = self.display_manager.extra_small_font if hasattr(self.display_manager, 'extra_small_font') else ImageFont.load_default()


# Fonts/colors/offsets honor customization.<element> (classic
# PressStart2P@8 / 4x6@6 when untouched)
(title_font, title_color, (title_dx, title_dy),
body_font, body_color, (body_dx, body_dy)) = self._element_styles()

# Get font heights
try:
title_height = self.display_manager.get_font_height(title_font)
Expand Down Expand Up @@ -569,24 +610,24 @@ def _display_content(self, category_config: Dict, item_data: Dict):
title_width = len(title) * 6

# Center the title horizontally (same position as in _display_title)
title_x = (self.display_manager.width - title_width) // 2
title_y = margin_top
title_x = (self.display_manager.width - title_width) // 2 + title_dx
title_y = margin_top + title_dy

# Draw title using display_manager.draw_text (same as title screen)
self.display_manager.draw_text(
title,
x=title_x,
y=title_y,
color=self.title_color,
color=title_color,
font=title_font
)

# Draw underline below title (same as title screen)
underline_y = title_y + title_height + 1
underline_x_start = title_x
underline_x_end = title_x + title_width
draw.line([(underline_x_start, underline_y), (underline_x_end, underline_y)],
fill=self.title_color, width=1)
draw.line([(underline_x_start, underline_y), (underline_x_end, underline_y)],
fill=title_color, width=1)

# Wrap description text
available_width = self.display_manager.width - 4
Expand Down Expand Up @@ -625,14 +666,14 @@ def _display_content(self, category_config: Dict, item_data: Dict):
line_width = bbox[2] - bbox[0]
else:
line_width = len(line) * 6
line_x = (self.display_manager.width - line_width) // 2
line_x = (self.display_manager.width - line_width) // 2 + body_dx

# Use display_manager.draw_text for description
self.display_manager.draw_text(
line,
x=line_x,
y=current_y,
color=self.subtitle_color,
y=current_y + body_dy,
color=body_color,
font=body_font
)

Expand Down
2 changes: 1 addition & 1 deletion plugins/of-the-day/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "of-the-day",
"name": "Of The Day Display",
"version": "1.2.0",
"version": "1.3.0",
"author": "ChuckBuilds",
"description": "Display daily featured content like Word of the Day, Bible verses, or custom daily items. Supports multiple categories with rotating display and configurable data sources.",
"category": "information",
Expand Down
Loading
Loading