-
-
Notifications
You must be signed in to change notification settings - Fork 563
add widget overlay system with some sample widgets #733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nunomdc
wants to merge
7
commits into
fatihak:main
Choose a base branch
from
nunomdc:wip-screen_overlays
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
17238cd
Initial widgets implementation
nunomdc 92b5695
Add IP address widget and improve widget system robustness
nunomdc 18eb034
add widget install support to cli scripts
nunomdc 0504f8a
add building_widgets guide
nunomdc 246f603
Apply suggestions from code review
nunomdc 43d5385
Updated cli scripts help
nunomdc 8e6f8ab
Apply suggestions from code review
nunomdc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| # Building InkyPi Widgets | ||
|
|
||
| This guide walks you through the process of creating a new widget for InkyPi. Widgets are small overlay elements that can be displayed on top of plugin-generated images, such as date stamps, status indicators, or custom messages. | ||
|
|
||
| ## What are Widgets? | ||
|
|
||
| Widgets are lightweight overlay components that: | ||
| - Render on top of the active plugin image | ||
| - Can be positioned in any corner of the display | ||
| - Support automatic contrast color calculation for readability | ||
| - Can be enabled/disabled and reordered through the web UI | ||
|
|
||
| ## Creating a Widget | ||
|
|
||
| ### 1. Create a Directory for Your Widget | ||
|
|
||
| - Navigate to the `src/widgets` directory. | ||
| - Create a new directory named after your widget. The directory name will be the `id` of your widget and should be all lowercase with no spaces. Example: | ||
|
|
||
| ```bash | ||
| mkdir src/widgets/date_widget | ||
| ``` | ||
|
|
||
| ### 2. Create a Python File and Class for the Widget | ||
|
|
||
| - Inside your new widget directory, create a Python file with the same name as the directory. | ||
| - Define a class in the file that inherits from `BaseWidget`. | ||
| - In your new class, implement the `generate_image` function: | ||
| - **Arguments:** | ||
| - `settings`: A dictionary of widget configuration values from the form inputs in the web UI. | ||
| - `device_config`: An instance of the Config class, used to retrieve device configurations such as display resolution or timezone. | ||
| - **Return:** A `PIL.Image` object in RGBA mode (with transparency) that will be overlaid on the main image. | ||
| - **Important:** Crop your image to the actual content size to ensure proper positioning. | ||
| - If there are any issues, raise a `RuntimeError` exception with a clear message. | ||
|
|
||
| Example widget implementation: | ||
|
|
||
| ```python | ||
| from widgets.base_widget.base_widget import BaseWidget | ||
| from utils.app_utils import get_font | ||
| from PIL import Image, ImageDraw | ||
| from datetime import datetime | ||
| import pytz | ||
|
|
||
| class DateWidget(BaseWidget): | ||
| def generate_image(self, settings, device_config): | ||
| # Create transparent overlay | ||
| overlay = Image.new('RGBA', (200, 80), (0, 0, 0, 0)) | ||
| draw = ImageDraw.Draw(overlay) | ||
|
|
||
| # Get settings | ||
| font_size = int(settings.get('font_size', 18)) | ||
| font = get_font("Jost", font_size) | ||
|
|
||
| # Get current date | ||
| tz = pytz.timezone(device_config.get_config('timezone', 'UTC')) | ||
| now = datetime.now(tz) | ||
| date_str = now.strftime('%Y-%m-%d') | ||
|
|
||
| # Use contrast color if enabled | ||
| use_contrast_color = settings.get('use_contrast_color', False) | ||
| if use_contrast_color: | ||
| text_color = settings.get('contrast_color', '#FFFFFF') | ||
| else: | ||
| text_color = settings.get('text_color', '#FFFFFF') | ||
|
|
||
| # Draw text | ||
| draw.text((0, 0), date_str, fill=text_color, font=font) | ||
|
|
||
| # Crop to actual text size | ||
| bbox = draw.textbbox((0, 0), date_str, font=font) | ||
| return overlay.crop(bbox) | ||
| ``` | ||
|
|
||
| ### 3. Create a Settings Template (Optional) | ||
|
|
||
| If your widget requires user configuration through the web UI, create a `settings.html` file in your widget directory: | ||
|
|
||
| ```html | ||
| <!-- use_contrast_color checkbox is automatically injected by the base widget template --> | ||
| <div class="form-group"> | ||
| <label for="font_size" class="form-label">Font Size</label> | ||
| <input type="number" name="font_size" class="form-input" | ||
| value="{{ plugin_settings.font_size | default(18) }}" | ||
| min="8" max="48"> | ||
| </div> | ||
|
|
||
| <div class="form-group" id="text-color-group"> | ||
| <label for="text_color" class="form-label">Text Color</label> | ||
| <input type="color" name="text_color" class="color-picker" | ||
| value="{{ plugin_settings.text_color | default('#FFFFFF') }}"> | ||
| </div> | ||
|
|
||
| <script> | ||
| // Hide text color picker when contrast color is enabled | ||
| document.addEventListener('DOMContentLoaded', () => { | ||
| const $textColorGroup = document.getElementById('text-color-group'); | ||
| const $contrastCheckbox = document.getElementById('use_contrast_color'); | ||
|
|
||
| $contrastCheckbox.addEventListener('change', (event) => { | ||
| if (event.target.checked) { | ||
| $textColorGroup.classList.add('hidden'); | ||
| } else { | ||
| $textColorGroup.classList.remove('hidden'); | ||
| } | ||
| }); | ||
| }); | ||
| </script> | ||
| ``` | ||
|
|
||
| ### 4. Create a Widget Info File | ||
|
|
||
| Create a `widget-info.json` file in your widget directory to register it with InkyPi: | ||
|
|
||
| ```json | ||
| { | ||
| "id": "date_widget", | ||
| "display_name": "Date Widget", | ||
| "description": "Displays current date", | ||
| "class": "DateWidget", | ||
| "repository": "https://github.com/your-username/your-widget-repo" | ||
| } | ||
| ``` | ||
|
|
||
| - **id**: Must match your directory name | ||
| - **display_name**: Display name shown in the web UI (required for widgets) | ||
| - **description**: Brief description of what the widget does | ||
| - **class**: The name of your Python class | ||
| - **repository**: Git URL of the widget repository (leave empty for built-in widgets) | ||
|
|
||
| ### 5. Override Settings Template Generation (Optional) | ||
|
|
||
| If your settings template requires additional variables, override the `generate_settings_template` function: | ||
|
|
||
| ```python | ||
| def generate_settings_template(self): | ||
| template_params = super().generate_settings_template() | ||
| template_params['date_formats'] = ['YYYY-MM-DD', 'DD-MM-YYYY', 'MM-DD-YYYY'] | ||
| return template_params | ||
| ``` | ||
|
|
||
| ## Widget Features | ||
|
|
||
| ### Automatic Contrast Color | ||
|
|
||
| Widgets support automatic contrast color calculation to ensure text is readable against any background: | ||
|
|
||
| 1. Enable the "Use Contrast Color" checkbox in the widget settings | ||
| 2. The system will analyze the background where the widget will be placed | ||
| 3. It automatically chooses black (#000000) or white (#FFFFFF) for optimal contrast | ||
| 4. The contrast color is passed to your widget in `settings['contrast_color']` | ||
|
|
||
| ### Positioning and Layout | ||
|
|
||
| Widgets can be positioned in any corner of the display: | ||
| - **Corners**: top-left, top-right, bottom-left, bottom-right | ||
| - **Orientation**: horizontal or vertical | ||
| - **Spacing**: Gap between multiple widgets (in pixels) | ||
| - **Margin**: Distance from the edge of the display (in pixels) | ||
|
|
||
| These settings are configured globally in the Widgets page and apply to all enabled widgets. | ||
|
|
||
| ### Widget Ordering | ||
|
|
||
| When multiple widgets are enabled, they are rendered in the order shown in the "Enabled Widgets" list. You can drag and drop to reorder them in the web UI. | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Keep it Small**: Widgets should be compact overlays, not full-screen images | ||
| 2. **Use Transparency**: Always use RGBA mode and transparent backgrounds | ||
| 3. **Crop to Content**: Crop your image to the actual content size for proper positioning | ||
| 4. **Test Contrast**: Test your widget with both light and dark backgrounds | ||
| 5. **Performance**: Keep widget generation fast since they're applied on every refresh | ||
|
|
||
| ## Serving Static Assets | ||
|
|
||
| If your widget needs to serve static files (images, CSS, etc.), place them in your widget directory and reference them using the widget asset route: | ||
|
|
||
| ```html | ||
| <img src="{{ url_for('widget.widget_asset', widget_id='your_widget_id', filename='icon.png') }}"> | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| #!/usr/bin/env bash | ||
| set -e | ||
|
|
||
| WIDGETS_DIR="$PROJECT_DIR/src/widgets" | ||
|
|
||
| command="${1:-}" | ||
| shift || true | ||
|
|
||
| usage() { | ||
| echo "" | ||
| echo "Widget commands:" | ||
| echo " inkypi widget install <widget_id> <git_repository_url>" | ||
| echo " inkypi widget remove <widget_id>" | ||
| echo " inkypi widget list" | ||
| echo "" | ||
| } | ||
|
|
||
| require_widgets_dir() { | ||
| if [[ ! -d "$WIDGETS_DIR" ]]; then | ||
| echo "Widgets directory does not exist: $WIDGETS_DIR" | ||
| exit 1 | ||
| fi | ||
| } | ||
|
|
||
| # ---------------------------- | ||
| # INSTALL | ||
| # ---------------------------- | ||
| install_widget() { | ||
| if [[ $# -ne 2 ]]; then | ||
| usage | ||
| exit 1 | ||
| fi | ||
|
|
||
| WIDGET_ID="$1" | ||
| REPO_URL="$2" | ||
| DEST_DIR="$WIDGETS_DIR/$WIDGET_ID" | ||
|
|
||
| require_widgets_dir | ||
|
|
||
| # Workaround for .pyc files created by inkypi app with sudo | ||
| if [[ -d "$DEST_DIR" ]]; then | ||
| sudo chown -R "$(whoami)":"$(whoami)" "$DEST_DIR" 2>/dev/null || true | ||
| chmod -R u+rw "$DEST_DIR" 2>/dev/null || true | ||
| fi | ||
|
|
||
| echo "[INFO] Installing widget '$WIDGET_ID' from $REPO_URL" | ||
|
|
||
| rm -rf "$DEST_DIR" | ||
| mkdir -p "$DEST_DIR" | ||
|
|
||
| git clone --depth 1 --filter=blob:none --sparse "$REPO_URL" "$DEST_DIR" &>/dev/null | ||
|
|
||
| # Check if the folder exists in the branch | ||
| DEFAULT_BRANCH=$(git -C "$DEST_DIR" symbolic-ref --short HEAD 2>/dev/null || echo main) | ||
| if ! git -C "$DEST_DIR" ls-tree --name-only "$DEFAULT_BRANCH" | grep -qx "$WIDGET_ID"; then | ||
| echo "[INFO] Widget '$WIDGET_ID' does not exist in the repo" | ||
| rm -rf "$DEST_DIR" | ||
| exit 1 | ||
| fi | ||
|
|
||
| cd "$DEST_DIR" | ||
| git sparse-checkout set "$WIDGET_ID" &>/dev/null | ||
|
|
||
| # Move widget files to root of DEST_DIR | ||
| shopt -s dotglob | ||
| mv "$WIDGET_ID"/* ./ | ||
| rm -rf "$WIDGET_ID" | ||
|
|
||
| echo "[INFO] Widget copied to $DEST_DIR" | ||
|
|
||
| # Install requirements if any | ||
| REQ_FILE="$DEST_DIR/requirements.txt" | ||
|
|
||
| if [[ -f "$REQ_FILE" ]]; then | ||
| echo "[INFO] requirements.txt found, installing dependencies..." | ||
|
|
||
| if [[ ! -d "$VENV_PATH" ]]; then | ||
| echo "[ERROR] Virtual environment not found at $VENV_PATH" | ||
| exit 1 | ||
| fi | ||
|
|
||
| source "$VENV_PATH/bin/activate" | ||
| pip install -r "$REQ_FILE" | ||
| deactivate | ||
|
|
||
| echo "[INFO] Dependencies installed" | ||
| else | ||
| echo "[INFO] No requirements.txt found, skipping dependency install" | ||
| fi | ||
|
|
||
| echo "[INFO] Restarting $APPNAME service." | ||
| sudo systemctl restart "$APPNAME.service" | ||
|
|
||
| echo "[INFO] Done" | ||
| } | ||
|
|
||
| # ---------------------------- | ||
| # REMOVE | ||
| # ---------------------------- | ||
| uninstall_widget() { | ||
| if [[ $# -ne 1 ]]; then | ||
| usage | ||
| exit 1 | ||
| fi | ||
|
|
||
| WIDGET_ID="$1" | ||
| DEST_DIR="$WIDGETS_DIR/$WIDGET_ID" | ||
|
|
||
| require_widgets_dir | ||
|
|
||
| if [[ ! -d "$DEST_DIR" ]]; then | ||
| echo "[ERROR] Widget '$WIDGET_ID' is not installed." | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "[INFO] Removing widget '$WIDGET_ID'" | ||
|
|
||
| sudo chown -R "$(whoami)":"$(whoami)" "$DEST_DIR" 2>/dev/null || true | ||
| rm -rf "$DEST_DIR" | ||
|
|
||
| echo "[INFO] Widget successfully uninstalled" | ||
|
|
||
| echo "[INFO] Restarting $APPNAME service." | ||
| sudo systemctl restart "$APPNAME.service" | ||
| } | ||
|
|
||
| # ---------------------------- | ||
| # LIST | ||
| # ---------------------------- | ||
| list_widgets() { | ||
| require_widgets_dir | ||
| { | ||
| printf "WIDGET\tNAME\tTYPE\tREPOSITORY\n" | ||
|
|
||
| for widget_dir in "$WIDGETS_DIR"/*; do | ||
| widget_id="$(basename "$widget_dir")" | ||
| [[ "$widget_id" == "base_widget" ]] && continue | ||
|
|
||
| info_file="$widget_dir/widget-info.json" | ||
| [[ ! -f "$info_file" ]] && continue | ||
|
|
||
| # Extract name + repository | ||
| IFS=$'\t' read -r name repo < <( | ||
| jq -r '[.display_name, .repository] | map(. // "") | @tsv' "$info_file" | ||
| ) | ||
|
|
||
| # Infer type from repository presence | ||
| if [[ -n "$repo" ]]; then | ||
| type="third_party" | ||
| else | ||
| type="builtin" | ||
| repo="-" | ||
| fi | ||
|
|
||
| printf "%s\t%s\t%s\t%s\n" \ | ||
| "$widget_id" "$name" "$type" "$repo" | ||
| done | ||
| } | column -t -s $'\t' | ||
| } | ||
| # ---------------------------- | ||
| # COMMAND ROUTER | ||
| # ---------------------------- | ||
|
|
||
| case "$command" in | ||
| install) | ||
| install_widget "$@" | ||
| ;; | ||
| uninstall) | ||
| uninstall_widget "$@" | ||
| ;; | ||
| list) | ||
| list_widgets | ||
| ;; | ||
| ""|-h|--help|help) | ||
| usage | ||
| ;; | ||
| *) | ||
| echo "[ERROR] Unknown command: $command" | ||
| usage | ||
| exit 1 | ||
| ;; | ||
| esac | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.