mirror of
https://github.com/alexhopeoconnor/bom-local-card.git
synced 2026-10-03 22:22:00 +10:00
Update card UI styling and editor enhancements
- Improve error display styling and button appearance - Enhance editor with expanded metadata and controls configuration - Update types and README documentation
This commit is contained in:
@@ -166,11 +166,13 @@ For more detailed service setup and configuration options, see the [BOM Local Se
|
||||
- Play/Pause Button
|
||||
- Previous/Next Buttons
|
||||
- Frame Slider
|
||||
- Navigation Buttons (-10, +10, First, Last)
|
||||
- Navigation Buttons (skip 1/3 of frames, First, Last)
|
||||
- Frame Info
|
||||
- **Overlay Controls on Image**: Toggle to overlay controls on the radar image
|
||||
- **Overlay Position**: Choose overlay position (Top, Bottom, Left, Right, Center)
|
||||
- **Overlay Opacity**: Control overlay transparency (0.0 to 1.0)
|
||||
- **Controls Position**: Choose where to display controls (Above Image, Below Image, Overlay on Image)
|
||||
- When position is "Overlay on Image":
|
||||
- **Controls Overlay Position**: Choose overlay position (Top, Bottom, Left, Right, Center)
|
||||
- **Controls Overlay Opacity**: Control overlay transparency (0.0 to 1.0)
|
||||
- **Conflict Detection**: Editor warns if metadata and controls overlays would conflict
|
||||
- **Image Zoom**: Zoom level for radar images (0.5 = 50%, 1.0 = 100%, 2.0 = 200%, range: 0.5-3.0)
|
||||
- **Image Fit**: How images fit in container (Contain, Cover, Fill)
|
||||
|
||||
@@ -182,7 +184,7 @@ For more detailed service setup and configuration options, see the [BOM Local Se
|
||||
**Auto Refresh**:
|
||||
- **Refresh Interval**: Seconds between automatic data refreshes (default: `30`, range: 10-300)
|
||||
|
||||
**Note**: For custom time ranges (using `timespan: custom`), you'll need to configure `custom_start_time` and `custom_end_time` via YAML as these options are not available in the visual editor.
|
||||
**Note**: For custom time ranges (using `timespan: custom`), the editor will show input fields for `custom_start_time` and `custom_end_time`. The editor also displays available cache range information and warns if your selected timespan exceeds available data.
|
||||
|
||||
### Using YAML
|
||||
|
||||
@@ -256,10 +258,12 @@ custom_end_time: "2024-01-15T14:00:00Z"
|
||||
| `show_play_pause` | boolean | `true` | Show play/pause button |
|
||||
| `show_prev_next` | boolean | `true` | Show previous/next buttons |
|
||||
| `show_slider` | boolean | `true` | Show frame slider |
|
||||
| `show_nav_buttons` | boolean | `true` | Show navigation buttons (First, -10, +10, Last) |
|
||||
| `show_nav_buttons` | boolean | `true` | Show navigation buttons (skip 1/3 of frames, First, Last) |
|
||||
| `show_frame_info` | boolean | `true` | Show frame information (frame number, timestamp) |
|
||||
| `position` | string | `below` | Where to display: `above`, `below`, or `overlay` |
|
||||
|
||||
**Note**: When `position` is set to `overlay`, use the top-level `controls_overlay_position` and `controls_overlay_opacity` options to control overlay placement and transparency.
|
||||
|
||||
#### Slideshow Configuration
|
||||
|
||||
| Option | Type | Default | Required | Description |
|
||||
@@ -307,22 +311,27 @@ show_metadata: false
|
||||
show_controls: false
|
||||
```
|
||||
|
||||
### Overlay Controls on Image
|
||||
### Overlay Controls and Metadata on Image
|
||||
|
||||
Overlay controls on the radar image to save space:
|
||||
Overlay both controls and metadata on the radar image to save space:
|
||||
|
||||
```yaml
|
||||
type: custom:bom-local-radar-card
|
||||
suburb: Brisbane
|
||||
state: QLD
|
||||
overlay_controls: true
|
||||
overlay_position: bottom
|
||||
overlay_opacity: 0.85
|
||||
show_controls:
|
||||
position: overlay
|
||||
controls_overlay_position: bottom
|
||||
controls_overlay_opacity: 0.9
|
||||
show_metadata:
|
||||
position: overlay
|
||||
style: minimal
|
||||
style: compact
|
||||
metadata_overlay_position: top
|
||||
metadata_overlay_opacity: 0.85
|
||||
```
|
||||
|
||||
**Note**: The editor will warn you if metadata and controls overlays are configured at conflicting positions (e.g., both at "top").
|
||||
|
||||
### Image Zoom
|
||||
|
||||
Zoom in on the radar image (may pixelate at higher zoom levels):
|
||||
@@ -356,7 +365,7 @@ show_metadata:
|
||||
|
||||
### Granular Controls Control
|
||||
|
||||
Show only specific controls:
|
||||
Show only specific controls and position them above the image:
|
||||
|
||||
```yaml
|
||||
type: custom:bom-local-radar-card
|
||||
@@ -368,6 +377,7 @@ show_controls:
|
||||
show_frame_info: true
|
||||
show_nav_buttons: false
|
||||
show_prev_next: false
|
||||
position: above
|
||||
```
|
||||
|
||||
### Historical Data (Last 3 Hours)
|
||||
@@ -430,11 +440,11 @@ The card provides several controls for navigating radar frames (all can be indiv
|
||||
- **Play/Pause Button**: Start or stop the animation
|
||||
- **Previous/Next Buttons**: Navigate to the previous or next frame
|
||||
- **Frame Slider**: Drag to jump to any frame
|
||||
- **Navigation Buttons**:
|
||||
- **Navigation Buttons** (only shown for timeseries data):
|
||||
- ⏮ First frame
|
||||
- -10 / +10: Jump backward/forward by 10 frames
|
||||
- -X / +X: Jump backward/forward by approximately 1/3 of total frames (dynamically calculated)
|
||||
- ⏭ Last frame
|
||||
- **Frame Info**: Displays frame number, total frames, observation time, and progress percentage
|
||||
- **Frame Info**: Compact display showing frame number, total frames, observation time, and progress percentage in a single line
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
@@ -594,49 +604,6 @@ To test the card against a local version of the BOM Local Service (instead of th
|
||||
|
||||
This builds the service from your local source and uses it in the test environment.
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 0.1.0 (Current)
|
||||
|
||||
**Major Improvements:**
|
||||
- ✨ **Enhanced Error Handling**: Full support for structured API error responses with detailed error codes, retry suggestions, and intelligent auto-retry functionality
|
||||
- 🔄 **Smart Retry Logic**: Action-based retry behavior - respects service recommendations for when to auto-retry vs. manual refresh
|
||||
- 🎨 **Redesigned Editor**: Complete overhaul of the visual editor with native dropdowns, better layout, and expandable sections
|
||||
- 🖼️ **Image Display Fixes**: Fixed image centering issues and added zoom/fit options
|
||||
- 🎛️ **Granular Configuration**: Fine-grained control over metadata and controls visibility
|
||||
- 📍 **Overlay Support**: Overlay controls and metadata on images to save dashboard space
|
||||
- ⌨️ **Keyboard Navigation**: Full keyboard support for accessibility
|
||||
- ♿ **Accessibility**: ARIA labels and screen reader support
|
||||
- 🔍 **Better Error Messages**: Detailed error information including previous update failures and available data ranges
|
||||
|
||||
**New Configuration Options:**
|
||||
- `show_card_title`: Control card title visibility
|
||||
- `show_metadata`: Object-based configuration for granular metadata control
|
||||
- `show_controls`: Object-based configuration for granular controls control
|
||||
- `image_zoom`: Zoom images from 0.5x to 3.0x
|
||||
- `image_fit`: Control how images fit (contain/cover/fill)
|
||||
- `overlay_controls`: Overlay controls on image
|
||||
- `overlay_position`: Control overlay position
|
||||
- `overlay_opacity`: Control overlay transparency
|
||||
|
||||
**Bug Fixes:**
|
||||
- Fixed editor dropdown crashes
|
||||
- Fixed image centering issues
|
||||
- Fixed editor layout problems
|
||||
- Improved error handling for fresh cache scenarios
|
||||
|
||||
**Service Integration:**
|
||||
- Enhanced compatibility with service's action-based error recommendations
|
||||
- Proper handling of `manual_refresh_recommended` action type
|
||||
- Respects service-calculated retry times based on update progress
|
||||
- Displays refresh endpoint URLs in error details
|
||||
|
||||
### Version 0.0.1 (Initial Release)
|
||||
|
||||
- Initial release with basic radar display functionality
|
||||
- Support for latest frames and historical data
|
||||
- Basic configuration options
|
||||
- Visual editor support
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+250
-81
@@ -187,6 +187,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
||||
border: 3px solid white;
|
||||
transition: all 0.2s;
|
||||
margin-top: -7px; /* Center the 24px thumb on the 10px track: (24-10)/2 = 7 */
|
||||
}
|
||||
.frame-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.15);
|
||||
@@ -220,20 +221,24 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.play-btn {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
border: 2px solid var(--primary-color, #667eea);
|
||||
background: var(--primary-color, #667eea);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.2s;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.play-btn:hover:not(:disabled) {
|
||||
background: var(--primary-color-dark, #5568d3);
|
||||
@@ -286,6 +291,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.1));
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
@@ -433,10 +439,13 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
.detail-content code {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
background: var(--card-background-color, #ffffff);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
background: var(--secondary-background-color, rgba(0, 0, 0, 0.05));
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.15));
|
||||
color: var(--primary-text-color, #212121);
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.error-detail-item.suggested-range {
|
||||
@@ -467,19 +476,35 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
min-height: 44px;
|
||||
background: var(--error-color);
|
||||
background: var(--error-color, #f44336);
|
||||
color: white !important;
|
||||
border: none;
|
||||
border: 2px solid var(--error-color, #f44336);
|
||||
border-radius: 8px;
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
font-family: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.retry-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0));
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.retry-button:hover {
|
||||
@@ -544,20 +569,71 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
.metadata-section.metadata-cards .metadata-item {
|
||||
background: var(--card-background-color, #ffffff);
|
||||
border: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.metadata-section.metadata-cards .metadata-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.metadata-section.metadata-cards .metadata-label {
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.metadata-section.metadata-cards .metadata-value {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metadata-section.metadata-compact {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.metadata-section.metadata-compact .metadata-item {
|
||||
background: var(--secondary-background-color, #f5f5f5);
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metadata-section.metadata-minimal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.metadata-section.metadata-minimal .metadata-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
|
||||
.metadata-section.metadata-minimal .metadata-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metadata-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -651,11 +727,31 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
.metadata-overlay.overlay-top {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.metadata-overlay.overlay-bottom {
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.metadata-overlay.overlay-left {
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.metadata-overlay.overlay-right {
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.metadata-overlay.overlay-center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Controls Section */
|
||||
@@ -701,6 +797,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
font-size: 0.9em;
|
||||
transition: all 0.2s;
|
||||
min-width: 50px;
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -743,6 +840,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
||||
border: 3px solid white;
|
||||
transition: all 0.2s;
|
||||
margin-top: -7px; /* Center the 24px thumb on the 10px track: (24-10)/2 = 7 */
|
||||
}
|
||||
|
||||
.frame-slider::-webkit-slider-thumb:hover {
|
||||
@@ -780,36 +878,53 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
|
||||
.play-controls {
|
||||
display: flex;
|
||||
flex-direction: column; /* Stack on mobile */
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.play-controls {
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Frame Info */
|
||||
.frame-info {
|
||||
text-align: center;
|
||||
color: var(--bom-secondary-color);
|
||||
font-size: 0.9em;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: var(--secondary-text-color, rgba(0, 0, 0, 0.6));
|
||||
font-size: 0.85em;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding: 6px 12px;
|
||||
background: var(--card-background-color, #fff);
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.frame-index {
|
||||
font-weight: 600;
|
||||
color: var(--bom-text-color);
|
||||
color: var(--primary-text-color, rgba(0, 0, 0, 0.87));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.frame-time {
|
||||
white-space: nowrap;
|
||||
color: var(--secondary-text-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
.observation-time {
|
||||
white-space: nowrap;
|
||||
color: var(--secondary-text-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
.frame-progress {
|
||||
white-space: nowrap;
|
||||
color: var(--secondary-text-color, rgba(0, 0, 0, 0.6));
|
||||
font-size: 0.9em;
|
||||
}
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
@@ -818,7 +933,6 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Legacy info-section for backward compatibility */
|
||||
.info-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
@@ -912,13 +1026,14 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
size += 1;
|
||||
}
|
||||
|
||||
// Add space for controls if shown below
|
||||
// Add space for controls if shown below (not overlay)
|
||||
const controlsConfig = this._getControlsConfig();
|
||||
if (controlsConfig && typeof controlsConfig !== 'boolean') {
|
||||
if (controlsConfig.position !== 'overlay' && !this._config.overlay_controls) {
|
||||
if (controlsConfig.position !== 'overlay') {
|
||||
size += 2; // +100px for controls
|
||||
}
|
||||
} else if (controlsConfig === true && !this._config.overlay_controls) {
|
||||
} else if (controlsConfig === true) {
|
||||
// Default position is 'below', so add space
|
||||
size += 2;
|
||||
}
|
||||
|
||||
@@ -982,7 +1097,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
// Don't clear error immediately - let it fade out smoothly after data loads
|
||||
|
||||
// Check if we're in extended mode (historical data)
|
||||
const timespan = this._config.timespan || 'latest';
|
||||
@@ -1130,6 +1245,9 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
throw new Error('No frames available in response');
|
||||
}
|
||||
|
||||
// Clear error only after successful data load to prevent glitch
|
||||
this.error = undefined;
|
||||
|
||||
// Resolve relative image URLs against service URL
|
||||
// Service returns relative URLs like "/api/radar/.../frame/..."
|
||||
// which need to be absolute for browser to fetch from correct origin
|
||||
@@ -1382,6 +1500,9 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
nextUpdateTime: metadata.nextUpdateTime || endTime.toISOString(),
|
||||
};
|
||||
|
||||
// Clear error only after successful data load to prevent glitch
|
||||
this.error = undefined;
|
||||
|
||||
this.radarData = radarResponse;
|
||||
this.frames = allFrames;
|
||||
this.isExtendedMode = true;
|
||||
@@ -1546,7 +1667,8 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
if (!isoString) return '-';
|
||||
const date = new Date(isoString);
|
||||
const locale = this._getLocale();
|
||||
const timeZone = this.hass?.config?.time_zone || 'Australia/Brisbane';
|
||||
// Use HA timezone if available, otherwise use browser's timezone
|
||||
const timeZone = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
return date.toLocaleString(locale, {
|
||||
timeZone: timeZone,
|
||||
@@ -1870,19 +1992,21 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
const displayConfig = typeof config === 'object' ? config : {};
|
||||
const configPosition = displayConfig.position || 'above';
|
||||
// Default position is 'above' if not specified
|
||||
const configPosition = displayConfig.position ?? 'above';
|
||||
|
||||
// Only render if position matches
|
||||
if (configPosition !== position) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// If overlay, render differently
|
||||
// If overlay, render differently (overlay doesn't use style, it has its own compact format)
|
||||
if (position === 'overlay') {
|
||||
return this._renderOverlayMetadata();
|
||||
}
|
||||
|
||||
const style = displayConfig.style || 'cards';
|
||||
// Default style is 'cards' if not specified
|
||||
const style = displayConfig.style ?? 'cards';
|
||||
|
||||
return html`
|
||||
<div class="metadata-section metadata-${position} metadata-${style}">
|
||||
@@ -1987,21 +2111,25 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
*/
|
||||
private _renderOverlayMetadata(): TemplateResult {
|
||||
const config = this._getMetadataConfig();
|
||||
if (!config || typeof config !== 'object' || config.position !== 'overlay') {
|
||||
if (!config || (typeof config === 'boolean' && !config)) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const opacity = this._config.overlay_opacity ?? 0.85;
|
||||
const position = this._config.overlay_position || 'top';
|
||||
// Handle both object config and boolean true (default)
|
||||
const displayConfig = typeof config === 'object' ? config : {};
|
||||
|
||||
// Use separate metadata overlay settings if available, otherwise fall back to controls overlay settings
|
||||
const opacity = this._config.metadata_overlay_opacity ?? 0.85;
|
||||
const position = this._config.metadata_overlay_position ?? 'top';
|
||||
|
||||
return html`
|
||||
<div class="metadata-overlay overlay-${position}" style="opacity: ${opacity};">
|
||||
${config.show_cache_status !== false ? this._renderCacheStatus() : ''}
|
||||
${config.show_observation_time !== false ? this._renderObservationTime() : ''}
|
||||
${config.show_forecast_time !== false ? this._renderForecastTime() : ''}
|
||||
${config.show_weather_station !== false ? this._renderWeatherStation() : ''}
|
||||
${config.show_distance !== false ? this._renderDistance() : ''}
|
||||
${config.show_next_update !== false ? this._renderNextUpdate() : ''}
|
||||
${displayConfig.show_cache_status !== false ? this._renderCacheStatus() : ''}
|
||||
${displayConfig.show_observation_time !== false ? this._renderObservationTime() : ''}
|
||||
${displayConfig.show_forecast_time !== false ? this._renderForecastTime() : ''}
|
||||
${displayConfig.show_weather_station !== false ? this._renderWeatherStation() : ''}
|
||||
${displayConfig.show_distance !== false ? this._renderDistance() : ''}
|
||||
${displayConfig.show_next_update !== false ? this._renderNextUpdate() : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -2012,7 +2140,10 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
private _renderRadarImage(): TemplateResult {
|
||||
const zoom = this._config.image_zoom || 1.0;
|
||||
const fit = this._config.image_fit || 'contain';
|
||||
const overlayControls = this._config.overlay_controls || false;
|
||||
const controlsConfig = this._getControlsConfig();
|
||||
const controlsPosition = typeof controlsConfig === 'object' && controlsConfig.position === 'overlay';
|
||||
const metadataConfig = this._getMetadataConfig();
|
||||
const metadataPosition = typeof metadataConfig === 'object' && metadataConfig.position === 'overlay';
|
||||
const currentFrameUrl = this.getCurrentFrameUrl();
|
||||
|
||||
return html`
|
||||
@@ -2040,8 +2171,8 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
};
|
||||
}}"
|
||||
/>
|
||||
${overlayControls ? this._renderOverlayControls() : ''}
|
||||
${this._renderOverlayMetadata()}
|
||||
${controlsPosition ? this._renderOverlayControls() : ''}
|
||||
${metadataPosition ? this._renderOverlayMetadata() : ''}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
@@ -2059,8 +2190,8 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
const displayConfig = typeof config === 'object' ? config : {};
|
||||
const position = this._config.overlay_position || 'bottom';
|
||||
const opacity = this._config.overlay_opacity ?? 0.9;
|
||||
const position = this._config.controls_overlay_position ?? 'bottom';
|
||||
const opacity = this._config.controls_overlay_opacity ?? 0.9;
|
||||
|
||||
return html`
|
||||
<div class="controls-overlay overlay-${position}" style="opacity: ${opacity};">
|
||||
@@ -2075,22 +2206,28 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
/**
|
||||
* Render controls section
|
||||
*/
|
||||
private _renderControls(): TemplateResult {
|
||||
private _renderControls(position?: 'above' | 'below' | 'overlay'): TemplateResult {
|
||||
const config = this._getControlsConfig();
|
||||
if (!config || (typeof config === 'boolean' && !config)) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
// If overlay is enabled, controls are rendered in overlay
|
||||
if (this._config.overlay_controls) {
|
||||
const displayConfig = typeof config === 'object' ? config : {};
|
||||
// Default position is 'below' if not specified
|
||||
const configPosition = displayConfig.position ?? 'below';
|
||||
|
||||
// Only render if position matches (or if no position filter specified)
|
||||
if (position !== undefined && configPosition !== position) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const displayConfig = typeof config === 'object' ? config : {};
|
||||
const position = displayConfig.position || 'below';
|
||||
// If overlay, render differently
|
||||
if (configPosition === 'overlay') {
|
||||
return this._renderOverlayControls();
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="controls-section controls-${position}">
|
||||
<div class="controls-section controls-${configPosition}">
|
||||
${displayConfig.show_slider !== false ? this._renderFrameSlider() : ''}
|
||||
${displayConfig.show_nav_buttons !== false ? this._renderNavButtons() : ''}
|
||||
${displayConfig.show_play_pause !== false ? this._renderPlayPause() : ''}
|
||||
@@ -2125,12 +2262,19 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
|
||||
/**
|
||||
* Render navigation buttons
|
||||
* Only shows skip buttons for timeseries data (extended mode)
|
||||
*/
|
||||
private _renderNavButtons(): TemplateResult {
|
||||
if (this.frames.length === 0) return html``;
|
||||
|
||||
// Calculate dynamic skip amount based on total frames (1/3 of total, min 1, max 50)
|
||||
const skipAmount = this.isExtendedMode
|
||||
? Math.max(1, Math.min(50, Math.round(this.frames.length / 3)))
|
||||
: 0;
|
||||
|
||||
return html`
|
||||
<div class="frame-nav-buttons">
|
||||
${this.isExtendedMode ? html`
|
||||
<button
|
||||
class="frame-nav-btn"
|
||||
@click="${() => this.showFrame(0)}"
|
||||
@@ -2140,18 +2284,18 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
>⏮</button>
|
||||
<button
|
||||
class="frame-nav-btn"
|
||||
@click="${() => this.jumpFrame(-10)}"
|
||||
@click="${() => this.jumpFrame(-skipAmount)}"
|
||||
?disabled="${this.currentFrameIndex === 0}"
|
||||
title="Go back 10 frames"
|
||||
aria-label="Go back 10 frames"
|
||||
>-10</button>
|
||||
title="Go back ${skipAmount} frames"
|
||||
aria-label="Go back ${skipAmount} frames"
|
||||
>-${skipAmount}</button>
|
||||
<button
|
||||
class="frame-nav-btn"
|
||||
@click="${() => this.jumpFrame(10)}"
|
||||
@click="${() => this.jumpFrame(skipAmount)}"
|
||||
?disabled="${this.currentFrameIndex >= this.frames.length - 1}"
|
||||
title="Go forward 10 frames"
|
||||
aria-label="Go forward 10 frames"
|
||||
>+10</button>
|
||||
title="Go forward ${skipAmount} frames"
|
||||
aria-label="Go forward ${skipAmount} frames"
|
||||
>+${skipAmount}</button>
|
||||
<button
|
||||
class="frame-nav-btn"
|
||||
@click="${() => this.showFrame(this.frames.length - 1)}"
|
||||
@@ -2159,6 +2303,22 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
title="Last frame"
|
||||
aria-label="Last frame"
|
||||
>⏭</button>
|
||||
` : html`
|
||||
<button
|
||||
class="frame-nav-btn"
|
||||
@click="${() => this.showFrame(0)}"
|
||||
?disabled="${this.currentFrameIndex === 0}"
|
||||
title="First frame"
|
||||
aria-label="First frame"
|
||||
>⏮</button>
|
||||
<button
|
||||
class="frame-nav-btn"
|
||||
@click="${() => this.showFrame(this.frames.length - 1)}"
|
||||
?disabled="${this.currentFrameIndex >= this.frames.length - 1}"
|
||||
title="Last frame"
|
||||
aria-label="Last frame"
|
||||
>⏭</button>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -2168,6 +2328,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
*/
|
||||
private _renderPlayPause(): TemplateResult {
|
||||
return html`
|
||||
<div class="play-controls">
|
||||
<button
|
||||
class="play-btn"
|
||||
@click="${() => this.toggleAnimation()}"
|
||||
@@ -2176,6 +2337,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
>
|
||||
${this.isPlaying ? '⏸ Pause' : '▶ Play'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -2223,22 +2385,22 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
<div class="frame-info">
|
||||
<div class="frame-index">${frameInfoText}</div>
|
||||
<span class="frame-index">${frameInfoText}</span>
|
||||
${showFrameTimes && currentFrame.absoluteObservationTime ? html`
|
||||
<div class="frame-time">
|
||||
<span class="frame-time">
|
||||
${this.formatTimestamp(currentFrame.absoluteObservationTime)}
|
||||
</div>
|
||||
</span>
|
||||
` : showFrameTimes && currentFrame.minutesAgo !== undefined ? html`
|
||||
<div class="frame-time">
|
||||
${currentFrame.minutesAgo} minutes ago
|
||||
</div>
|
||||
<span class="frame-time">
|
||||
${currentFrame.minutesAgo} min ago
|
||||
</span>
|
||||
` : ''}
|
||||
${this.radarData?.observationTime && showFrameTimes ? html`
|
||||
<div class="observation-time">
|
||||
<span class="observation-time">
|
||||
Obs: ${this.formatTimestamp(this.radarData.observationTime)}
|
||||
</div>
|
||||
</span>
|
||||
` : ''}
|
||||
<div class="frame-progress">Progress: ${progress}%</div>
|
||||
<span class="frame-progress">${progress}%</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -2317,7 +2479,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
<div class="error-detail-item">
|
||||
<ha-icon class="detail-icon" icon="mdi:database-clock"></ha-icon>
|
||||
<div class="detail-content">
|
||||
<strong>Available Data Range:</strong> ${this.error.details.availableRange.oldest || 'N/A'} to ${this.error.details.availableRange.newest || 'N/A'}
|
||||
<strong>Available Data Range:</strong> ${this.error.details.availableRange.oldest ? this.formatTimestamp(this.error.details.availableRange.oldest) : 'N/A'} to ${this.error.details.availableRange.newest ? this.formatTimestamp(this.error.details.availableRange.newest) : 'N/A'}
|
||||
${this.error.details.availableRange.totalCacheFolders ? html` (${this.error.details.availableRange.totalCacheFolders} folders)` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2326,7 +2488,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
<div class="error-detail-item">
|
||||
<ha-icon class="detail-icon" icon="mdi:calendar-range"></ha-icon>
|
||||
<div class="detail-content">
|
||||
<strong>Requested Range:</strong> ${this.error.details.requestedRange.start || 'N/A'} to ${this.error.details.requestedRange.end || 'N/A'}
|
||||
<strong>Requested Range:</strong> ${this.error.details.requestedRange.start ? this.formatTimestamp(this.error.details.requestedRange.start) : 'N/A'} to ${this.error.details.requestedRange.end ? this.formatTimestamp(this.error.details.requestedRange.end) : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -2334,7 +2496,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
<div class="error-detail-item suggested-range">
|
||||
<ha-icon class="detail-icon" icon="mdi:lightbulb-on"></ha-icon>
|
||||
<div class="detail-content">
|
||||
<strong>Suggested Range:</strong> ${this.error.details.suggestedRange.start || 'N/A'} to ${this.error.details.suggestedRange.end || 'N/A'}
|
||||
<strong>Suggested Range:</strong> ${this.error.details.suggestedRange.start ? this.formatTimestamp(this.error.details.suggestedRange.start) : 'N/A'} to ${this.error.details.suggestedRange.end ? this.formatTimestamp(this.error.details.suggestedRange.end) : 'N/A'}
|
||||
<div class="suggestion-hint">Try using this time range instead</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2355,6 +2517,7 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
<button
|
||||
class="retry-button"
|
||||
type="button"
|
||||
style="--error-color: ${errorColor}"
|
||||
@click=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -2385,9 +2548,10 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
>
|
||||
<div id="root">
|
||||
${this._renderMetadata('above')}
|
||||
${this._renderControls('above')}
|
||||
${this._renderRadarImage()}
|
||||
${this._renderMetadata('below')}
|
||||
${this._renderControls()}
|
||||
${this._renderControls('below')}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -2398,7 +2562,12 @@ export class BomLocalRadarCard extends LitElement implements LovelaceCard {
|
||||
*/
|
||||
private _getRootClasses(): string {
|
||||
const classes: string[] = [];
|
||||
if (this._config.overlay_controls) {
|
||||
const controlsConfig = this._getControlsConfig();
|
||||
const controlsPosition = typeof controlsConfig === 'object' && controlsConfig.position === 'overlay';
|
||||
const metadataConfig = this._getMetadataConfig();
|
||||
const metadataPosition = typeof metadataConfig === 'object' && metadataConfig.position === 'overlay';
|
||||
|
||||
if (controlsPosition || metadataPosition) {
|
||||
classes.push('overlay-enabled');
|
||||
}
|
||||
return classes.join(' ');
|
||||
|
||||
+501
-56
@@ -16,12 +16,27 @@ const AUSTRALIAN_STATES = [
|
||||
{ value: 'WA', label: 'Western Australia' },
|
||||
];
|
||||
|
||||
interface CacheRange {
|
||||
oldestCache?: {
|
||||
cacheFolderName: string;
|
||||
cacheTimestamp: string;
|
||||
};
|
||||
newestCache?: {
|
||||
cacheFolderName: string;
|
||||
cacheTimestamp: string;
|
||||
};
|
||||
totalCacheFolders: number;
|
||||
timeSpanMinutes?: number;
|
||||
}
|
||||
|
||||
@customElement('bom-local-radar-card-editor')
|
||||
export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@state() private _config: BomLocalRadarCardConfig = this._mergeWithDefaults();
|
||||
@state() private _metadataExpanded = false;
|
||||
@state() private _controlsExpanded = false;
|
||||
@state() private _cacheRange?: CacheRange;
|
||||
@state() private _cacheRangeError?: string;
|
||||
|
||||
private _mergeWithDefaults(config: Partial<BomLocalRadarCardConfig> = {}): BomLocalRadarCardConfig {
|
||||
const defaults: Partial<BomLocalRadarCardConfig> = {
|
||||
@@ -35,7 +50,6 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
show_controls: true,
|
||||
image_zoom: 1.0,
|
||||
image_fit: 'contain',
|
||||
overlay_opacity: 0.9,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -90,14 +104,16 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
|
||||
<div class="section">
|
||||
<h3>Display</h3>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Show Card Title</label>
|
||||
<ha-switch
|
||||
label="Show Card Title"
|
||||
.checked=${this._config.show_card_title !== false}
|
||||
@change=${(e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
this._updateConfig('show_card_title', checked);
|
||||
}}
|
||||
></ha-switch>
|
||||
</div>
|
||||
${this._config.show_card_title !== false ? html`
|
||||
<ha-textfield
|
||||
label="Card Title"
|
||||
@@ -109,14 +125,16 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
|
||||
<div class="metadata-section">
|
||||
<div class="section-header">
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Show Metadata</label>
|
||||
<ha-switch
|
||||
label="Show Metadata"
|
||||
.checked=${this._getMetadataEnabled()}
|
||||
@change=${(e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
this._updateMetadataToggle(checked);
|
||||
}}
|
||||
></ha-switch>
|
||||
</div>
|
||||
${this._getMetadataEnabled() ? html`
|
||||
<ha-icon-button
|
||||
.label=${this._metadataExpanded ? 'Collapse' : 'Expand'}
|
||||
@@ -128,85 +146,154 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
</div>
|
||||
${this._getMetadataEnabled() && this._metadataExpanded ? html`
|
||||
<div class="metadata-options">
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Cache Status</label>
|
||||
<ha-switch
|
||||
label="Cache Status"
|
||||
.checked=${this._getMetadataConfig('show_cache_status')}
|
||||
@change=${(e: Event) => this._updateMetadataConfig('show_cache_status', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Observation Time</label>
|
||||
<ha-switch
|
||||
label="Observation Time"
|
||||
.checked=${this._getMetadataConfig('show_observation_time')}
|
||||
@change=${(e: Event) => this._updateMetadataConfig('show_observation_time', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Forecast Time</label>
|
||||
<ha-switch
|
||||
label="Forecast Time"
|
||||
.checked=${this._getMetadataConfig('show_forecast_time')}
|
||||
@change=${(e: Event) => this._updateMetadataConfig('show_forecast_time', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Weather Station</label>
|
||||
<ha-switch
|
||||
label="Weather Station"
|
||||
.checked=${this._getMetadataConfig('show_weather_station')}
|
||||
@change=${(e: Event) => this._updateMetadataConfig('show_weather_station', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Distance</label>
|
||||
<ha-switch
|
||||
label="Distance"
|
||||
.checked=${this._getMetadataConfig('show_distance')}
|
||||
@change=${(e: Event) => this._updateMetadataConfig('show_distance', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Next Update</label>
|
||||
<ha-switch
|
||||
label="Next Update"
|
||||
.checked=${this._getMetadataConfig('show_next_update')}
|
||||
@change=${(e: Event) => this._updateMetadataConfig('show_next_update', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Frame Times</label>
|
||||
<ha-switch
|
||||
label="Frame Times"
|
||||
.checked=${this._getMetadataConfig('show_frame_times')}
|
||||
@change=${(e: Event) => this._updateMetadataConfig('show_frame_times', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="select-wrapper">
|
||||
<label class="select-label">Metadata Position</label>
|
||||
<label class="select-label">Metadata Position *</label>
|
||||
<select
|
||||
class="native-select"
|
||||
.value=${this._getMetadataConfig('position') || 'above'}
|
||||
.value=${String(this._getMetadataConfig('position') ?? 'above')}
|
||||
@change=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const select = e.target as HTMLSelectElement;
|
||||
this._updateMetadataConfig('position', select.value as 'above' | 'below' | 'overlay');
|
||||
const value = select.value;
|
||||
if (value && value !== '') {
|
||||
this._updateMetadataConfig('position', value as 'above' | 'below' | 'overlay');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating metadata position:', err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="above">Above Image</option>
|
||||
<option value="above">Above Image (Default)</option>
|
||||
<option value="below">Below Image</option>
|
||||
<option value="overlay">Overlay on Image</option>
|
||||
</select>
|
||||
</div>
|
||||
${this._getMetadataConfig('position') === 'overlay' ? html`
|
||||
<div class="select-wrapper">
|
||||
<label class="select-label">Metadata Style</label>
|
||||
<label class="select-label">Metadata Overlay Position *</label>
|
||||
<select
|
||||
class="native-select"
|
||||
.value=${this._getMetadataConfig('style') || 'cards'}
|
||||
.value=${this._config.metadata_overlay_position ?? 'top'}
|
||||
@change=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const select = e.target as HTMLSelectElement;
|
||||
this._updateMetadataConfig('style', select.value as 'cards' | 'compact' | 'minimal');
|
||||
this._updateConfig('metadata_overlay_position', select.value);
|
||||
}}
|
||||
>
|
||||
<option value="cards">Cards</option>
|
||||
<option value="top">Top (Default)</option>
|
||||
<option value="bottom">Bottom</option>
|
||||
<option value="left">Left</option>
|
||||
<option value="right">Right</option>
|
||||
<option value="center">Center</option>
|
||||
</select>
|
||||
</div>
|
||||
<ha-textfield
|
||||
label="Metadata Overlay Opacity (0.0 - 1.0)"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
.value=${String(this._config.metadata_overlay_opacity ?? 0.85)}
|
||||
@input=${(e: Event) => this._updateConfig('metadata_overlay_opacity', parseFloat((e.target as HTMLInputElement).value))}
|
||||
></ha-textfield>
|
||||
` : ''}
|
||||
${this._getMetadataConfig('position') !== 'overlay' ? html`
|
||||
<div class="select-wrapper">
|
||||
<label class="select-label">Metadata Style *</label>
|
||||
<select
|
||||
class="native-select"
|
||||
.value=${this._getMetadataConfig('style') ?? 'cards'}
|
||||
@change=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const select = e.target as HTMLSelectElement;
|
||||
const value = select.value;
|
||||
// Allow empty string to reset to default
|
||||
if (value === '') {
|
||||
this._updateMetadataConfig('style', undefined);
|
||||
} else {
|
||||
this._updateMetadataConfig('style', value as 'cards' | 'compact' | 'minimal');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="cards">Cards (Default)</option>
|
||||
<option value="compact">Compact</option>
|
||||
<option value="minimal">Minimal</option>
|
||||
</select>
|
||||
</div>
|
||||
` : html`
|
||||
<div class="info-text" style="padding: 8px; background: var(--info-color, rgba(33, 150, 243, 0.1)); border-radius: 4px; font-size: 0.875em;">
|
||||
<strong>Note:</strong> Overlay position uses a fixed compact style optimized for overlaying on images.
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="controls-section">
|
||||
<div class="section-header">
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Show Controls</label>
|
||||
<ha-switch
|
||||
label="Show Controls"
|
||||
.checked=${this._getControlsEnabled()}
|
||||
@change=${(e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
this._updateControlsToggle(checked);
|
||||
}}
|
||||
></ha-switch>
|
||||
</div>
|
||||
${this._getControlsEnabled() ? html`
|
||||
<ha-icon-button
|
||||
.label=${this._controlsExpanded ? 'Collapse' : 'Expand'}
|
||||
@@ -218,66 +305,106 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
</div>
|
||||
${this._getControlsEnabled() && this._controlsExpanded ? html`
|
||||
<div class="controls-options">
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Play/Pause Button</label>
|
||||
<ha-switch
|
||||
label="Play/Pause Button"
|
||||
.checked=${this._getControlsConfig('show_play_pause')}
|
||||
@change=${(e: Event) => this._updateControlsConfig('show_play_pause', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Previous/Next Buttons</label>
|
||||
<ha-switch
|
||||
label="Previous/Next Buttons"
|
||||
.checked=${this._getControlsConfig('show_prev_next')}
|
||||
@change=${(e: Event) => this._updateControlsConfig('show_prev_next', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Frame Slider</label>
|
||||
<ha-switch
|
||||
label="Frame Slider"
|
||||
.checked=${this._getControlsConfig('show_slider')}
|
||||
@change=${(e: Event) => this._updateControlsConfig('show_slider', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Navigation Buttons (-10, +10, First, Last)</label>
|
||||
<ha-switch
|
||||
label="Navigation Buttons (-10, +10, First, Last)"
|
||||
.checked=${this._getControlsConfig('show_nav_buttons')}
|
||||
@change=${(e: Event) => this._updateControlsConfig('show_nav_buttons', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
</div>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Frame Info</label>
|
||||
<ha-switch
|
||||
label="Frame Info"
|
||||
.checked=${this._getControlsConfig('show_frame_info')}
|
||||
@change=${(e: Event) => this._updateControlsConfig('show_frame_info', (e.target as HTMLInputElement).checked)}
|
||||
></ha-switch>
|
||||
<ha-switch
|
||||
label="Overlay Controls on Image"
|
||||
.checked=${this._config.overlay_controls || false}
|
||||
@change=${(e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
this._updateConfig('overlay_controls', checked);
|
||||
}}
|
||||
></ha-switch>
|
||||
${this._config.overlay_controls ? html`
|
||||
</div>
|
||||
<div class="select-wrapper">
|
||||
<label class="select-label">Overlay Position</label>
|
||||
<label class="select-label">Controls Position *</label>
|
||||
<select
|
||||
class="native-select"
|
||||
.value=${this._config.overlay_position || 'bottom'}
|
||||
.value=${String(this._getControlsConfig('position') ?? 'below')}
|
||||
@change=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const select = e.target as HTMLSelectElement;
|
||||
this._updateConfig('overlay_position', select.value);
|
||||
const value = select.value;
|
||||
if (value && value !== '') {
|
||||
this._updateControlsConfig('position', value as 'above' | 'below' | 'overlay');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating controls position:', err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="above">Above Image</option>
|
||||
<option value="below">Below Image (Default)</option>
|
||||
<option value="overlay">Overlay on Image</option>
|
||||
</select>
|
||||
</div>
|
||||
${this._getControlsConfig('position') === 'overlay' ? html`
|
||||
<div class="select-wrapper">
|
||||
<label class="select-label">Controls Overlay Position *</label>
|
||||
<select
|
||||
class="native-select"
|
||||
.value=${this._config.controls_overlay_position ?? 'bottom'}
|
||||
@change=${(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const select = e.target as HTMLSelectElement;
|
||||
this._updateConfig('controls_overlay_position', select.value);
|
||||
} catch (err) {
|
||||
console.error('Error updating controls overlay position:', err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="top">Top</option>
|
||||
<option value="bottom">Bottom</option>
|
||||
<option value="bottom">Bottom (Default)</option>
|
||||
<option value="left">Left</option>
|
||||
<option value="right">Right</option>
|
||||
<option value="center">Center</option>
|
||||
</select>
|
||||
</div>
|
||||
<ha-textfield
|
||||
label="Overlay Opacity (0.0 - 1.0)"
|
||||
label="Controls Overlay Opacity (0.0 - 1.0)"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
.value=${String(this._config.overlay_opacity ?? 0.9)}
|
||||
@input=${(e: Event) => this._updateConfig('overlay_opacity', parseFloat((e.target as HTMLInputElement).value))}
|
||||
.value=${String(this._config.controls_overlay_opacity ?? 0.9)}
|
||||
@input=${(e: Event) => this._updateConfig('controls_overlay_opacity', parseFloat((e.target as HTMLInputElement).value))}
|
||||
></ha-textfield>
|
||||
${(() => {
|
||||
const conflict = this._checkOverlayConflict();
|
||||
return conflict.hasConflict ? html`
|
||||
<div class="warning-text" style="padding: 8px; background: var(--warning-color, rgba(255, 152, 0, 0.1)); border-left: 3px solid var(--warning-color, #ff9800); border-radius: 4px; font-size: 0.875em; margin-top: 8px;">
|
||||
${conflict.message}
|
||||
</div>
|
||||
` : '';
|
||||
})()}
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -319,18 +446,76 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
.value=${this._config.timespan || 'latest'}
|
||||
@change=${(e: Event) => {
|
||||
const select = e.target as HTMLSelectElement;
|
||||
this._updateConfig('timespan', select.value);
|
||||
const newTimespan = select.value as 'latest' | '1h' | '3h' | '6h' | '12h' | '24h' | 'custom';
|
||||
// Clear custom time fields if switching away from custom
|
||||
if (newTimespan !== 'custom' && this._config.timespan === 'custom') {
|
||||
const updatedConfig: BomLocalRadarCardConfig = { ...this._config };
|
||||
updatedConfig.timespan = newTimespan;
|
||||
delete updatedConfig.custom_start_time;
|
||||
delete updatedConfig.custom_end_time;
|
||||
this._config = updatedConfig;
|
||||
fireEvent(this, 'config-changed', { config: updatedConfig });
|
||||
} else {
|
||||
this._updateConfig('timespan', newTimespan);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="latest">Latest 7 frames</option>
|
||||
${this._cacheRange && this._cacheRange.totalCacheFolders > 0 ? html`
|
||||
<option value="1h">Last 1 hour${this._getRangeWarning('1h') ? ' ⚠️' : ''}</option>
|
||||
<option value="3h">Last 3 hours${this._getRangeWarning('3h') ? ' ⚠️' : ''}</option>
|
||||
<option value="6h">Last 6 hours${this._getRangeWarning('6h') ? ' ⚠️' : ''}</option>
|
||||
<option value="12h">Last 12 hours${this._getRangeWarning('12h') ? ' ⚠️' : ''}</option>
|
||||
<option value="24h">Last 24 hours${this._getRangeWarning('24h') ? ' ⚠️' : ''}</option>
|
||||
` : html`
|
||||
<option value="1h">Last 1 hour</option>
|
||||
<option value="3h">Last 3 hours</option>
|
||||
<option value="6h">Last 6 hours</option>
|
||||
<option value="12h">Last 12 hours</option>
|
||||
<option value="24h">Last 24 hours</option>
|
||||
`}
|
||||
<option value="custom">Custom time range</option>
|
||||
</select>
|
||||
</div>
|
||||
${this._cacheRange && this._cacheRange.totalCacheFolders > 0 ? html`
|
||||
<div class="cache-range-info">
|
||||
<div class="info-text">
|
||||
<strong>Available cache:</strong> ${this._formatCacheRange()}
|
||||
</div>
|
||||
</div>
|
||||
` : this._cacheRangeError ? html`
|
||||
<div class="cache-range-warning">
|
||||
<div class="warning-text">⚠️ ${this._cacheRangeError}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${this._getRangeWarning(this._config.timespan || 'latest') ? html`
|
||||
<div class="range-warning">
|
||||
<div class="warning-text">
|
||||
⚠️ Selected timespan may not have enough data. Available: ${this._formatCacheRange()}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${this._config.timespan === 'custom' ? html`
|
||||
<ha-textfield
|
||||
label="Custom Start Time (ISO 8601)"
|
||||
.value=${this._config.custom_start_time || ''}
|
||||
@input=${(e: Event) => this._updateConfig('custom_start_time', (e.target as HTMLInputElement).value)}
|
||||
helper="e.g., 2025-01-15T10:00:00Z"
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
label="Custom End Time (ISO 8601)"
|
||||
.value=${this._config.custom_end_time || ''}
|
||||
@input=${(e: Event) => this._updateConfig('custom_end_time', (e.target as HTMLInputElement).value)}
|
||||
helper="e.g., 2025-01-15T18:00:00Z"
|
||||
></ha-textfield>
|
||||
${this._validateCustomRange() ? html`
|
||||
<div class="range-warning">
|
||||
<div class="warning-text">
|
||||
⚠️ ${this._validateCustomRange()}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
<ha-textfield
|
||||
label="Frame Interval (seconds)"
|
||||
type="number"
|
||||
@@ -340,8 +525,9 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
.value=${String(this._config.frame_interval || 2.0)}
|
||||
@input=${(e: Event) => this._updateConfig('frame_interval', parseFloat((e.target as HTMLInputElement).value))}
|
||||
></ha-textfield>
|
||||
<div class="switch-wrapper">
|
||||
<label class="switch-label">Auto Play</label>
|
||||
<ha-switch
|
||||
label="Auto Play"
|
||||
.checked=${this._config.auto_play !== false}
|
||||
@change=${(e: Event) => {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
@@ -349,6 +535,7 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
}}
|
||||
></ha-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Auto Refresh</h3>
|
||||
@@ -395,18 +582,30 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
}
|
||||
}
|
||||
|
||||
private _getMetadataConfig(key: keyof MetadataDisplayConfig): boolean {
|
||||
private _getMetadataConfig(key: keyof MetadataDisplayConfig): boolean | string | undefined {
|
||||
const config = this._config.show_metadata;
|
||||
if (typeof config === 'boolean') {
|
||||
return config; // If boolean, all metadata follows this value
|
||||
// For boolean config, return default values for position/style, true for others
|
||||
if (key === 'position') return 'above';
|
||||
if (key === 'style') return 'cards';
|
||||
return config;
|
||||
}
|
||||
if (typeof config === 'object') {
|
||||
return config[key] !== false; // Default to true if not explicitly false
|
||||
const value = config[key];
|
||||
// For position/style, return the value or undefined (default)
|
||||
if (key === 'position' || key === 'style') {
|
||||
return value as string | undefined;
|
||||
}
|
||||
return true; // Default
|
||||
// For boolean fields, default to true if not explicitly false
|
||||
return value !== false;
|
||||
}
|
||||
// Default values
|
||||
if (key === 'position') return 'above';
|
||||
if (key === 'style') return 'cards';
|
||||
return true;
|
||||
}
|
||||
|
||||
private _updateMetadataConfig(key: keyof MetadataDisplayConfig, value: boolean | string): void {
|
||||
private _updateMetadataConfig(key: keyof MetadataDisplayConfig, value: boolean | string | undefined): void {
|
||||
let config: MetadataDisplayConfig;
|
||||
|
||||
if (typeof this._config.show_metadata === 'object') {
|
||||
@@ -415,14 +614,25 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
config = {};
|
||||
}
|
||||
|
||||
// Handle undefined to remove the key (revert to default)
|
||||
if (value === undefined) {
|
||||
delete (config as any)[key];
|
||||
} else {
|
||||
// Type-safe assignment based on key
|
||||
if (key === 'position' || key === 'style') {
|
||||
(config as any)[key] = value;
|
||||
(config as any)[key] = value as string;
|
||||
} else {
|
||||
(config as any)[key] = value;
|
||||
(config as any)[key] = value as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// If config is empty, set to true (default)
|
||||
if (Object.keys(config).length === 0) {
|
||||
this._updateConfig('show_metadata', true);
|
||||
} else {
|
||||
this._updateConfig('show_metadata', config);
|
||||
}
|
||||
}
|
||||
|
||||
// Controls configuration helpers
|
||||
private _getControlsEnabled(): boolean {
|
||||
@@ -447,18 +657,28 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
}
|
||||
}
|
||||
|
||||
private _getControlsConfig(key: keyof ControlsDisplayConfig): boolean {
|
||||
private _getControlsConfig(key: keyof ControlsDisplayConfig): boolean | string | undefined {
|
||||
const config = this._config.show_controls;
|
||||
if (typeof config === 'boolean') {
|
||||
// For boolean config, return default values for position, true for others
|
||||
if (key === 'position') return 'below';
|
||||
return config;
|
||||
}
|
||||
if (typeof config === 'object') {
|
||||
return config[key] !== false;
|
||||
const value = config[key];
|
||||
// For position, return the value or undefined (default)
|
||||
if (key === 'position') {
|
||||
return value as string | undefined;
|
||||
}
|
||||
return true; // Default
|
||||
// For boolean fields, default to true if not explicitly false
|
||||
return value !== false;
|
||||
}
|
||||
// Default values
|
||||
if (key === 'position') return 'below';
|
||||
return true;
|
||||
}
|
||||
|
||||
private _updateControlsConfig(key: keyof ControlsDisplayConfig, value: boolean): void {
|
||||
private _updateControlsConfig(key: keyof ControlsDisplayConfig, value: boolean | string | undefined): void {
|
||||
let config: ControlsDisplayConfig;
|
||||
|
||||
if (typeof this._config.show_controls === 'object') {
|
||||
@@ -467,10 +687,67 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
config = {};
|
||||
}
|
||||
|
||||
// All control config values are boolean (position is handled separately in template)
|
||||
(config as any)[key] = value;
|
||||
// Handle undefined to remove the key (revert to default)
|
||||
if (value === undefined) {
|
||||
delete (config as any)[key];
|
||||
} else {
|
||||
// Type-safe assignment based on key
|
||||
if (key === 'position') {
|
||||
(config as any)[key] = value as string;
|
||||
} else {
|
||||
(config as any)[key] = value as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
// If config is empty, set to true (default)
|
||||
if (Object.keys(config).length === 0) {
|
||||
this._updateConfig('show_controls', true);
|
||||
} else {
|
||||
this._updateConfig('show_controls', config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if metadata and controls would overlap when both are overlays
|
||||
*/
|
||||
private _checkOverlayConflict(): { hasConflict: boolean; message: string } {
|
||||
const metadataConfig = this._getMetadataConfig('position');
|
||||
const controlsConfig = this._getControlsConfig('position');
|
||||
|
||||
// Both must be overlays to have a conflict
|
||||
if (metadataConfig !== 'overlay' || controlsConfig !== 'overlay') {
|
||||
return { hasConflict: false, message: '' };
|
||||
}
|
||||
|
||||
const metadataPos = this._config.metadata_overlay_position ?? 'top';
|
||||
const controlsPos = this._config.controls_overlay_position ?? 'bottom';
|
||||
|
||||
// Check for conflicts
|
||||
if (metadataPos === controlsPos) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
message: `⚠️ Warning: Both metadata and controls are overlayed at the same position (${metadataPos}). They will overlap.`
|
||||
};
|
||||
}
|
||||
|
||||
// Check for adjacent positions that might conflict
|
||||
const conflicts: { [key: string]: string[] } = {
|
||||
'top': ['top', 'center'],
|
||||
'bottom': ['bottom', 'center'],
|
||||
'left': ['left', 'center'],
|
||||
'right': ['right', 'center'],
|
||||
'center': ['top', 'bottom', 'left', 'right', 'center']
|
||||
};
|
||||
|
||||
if (conflicts[metadataPos]?.includes(controlsPos)) {
|
||||
return {
|
||||
hasConflict: true,
|
||||
message: `⚠️ Warning: Metadata (${metadataPos}) and controls (${controlsPos}) may overlap. Consider using different positions.`
|
||||
};
|
||||
}
|
||||
|
||||
return { hasConflict: false, message: '' };
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
.editor {
|
||||
@@ -568,7 +845,7 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
box-shadow: 0 0 0 2px rgba(3, 169, 244, 0.2);
|
||||
}
|
||||
|
||||
ha-switch {
|
||||
.switch-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -576,10 +853,178 @@ export class BomLocalRadarCardEditor extends LitElement implements LovelaceCardE
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-text-color, #212121);
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ha-switch {
|
||||
margin-left: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
--mdc-icon-button-size: 32px;
|
||||
}
|
||||
|
||||
.cache-range-info {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--info-color, rgba(33, 150, 243, 0.1));
|
||||
border-left: 3px solid var(--info-color, #2196f3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cache-range-info .info-text {
|
||||
font-size: 0.875em;
|
||||
color: var(--primary-text-color, #212121);
|
||||
}
|
||||
|
||||
.range-warning,
|
||||
.cache-range-warning {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--warning-color, rgba(255, 152, 0, 0.1));
|
||||
border-left: 3px solid var(--warning-color, #ff9800);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.range-warning .warning-text,
|
||||
.cache-range-warning .warning-text {
|
||||
font-size: 0.875em;
|
||||
color: var(--primary-text-color, #212121);
|
||||
}
|
||||
`;
|
||||
|
||||
private _getRangeWarning(timespan: string): boolean {
|
||||
if (!this._cacheRange || this._cacheRange.totalCacheFolders === 0 || timespan === 'latest' || timespan === 'custom') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hours = parseInt(timespan.replace('h', '')) || 0;
|
||||
const requestedMinutes = hours * 60;
|
||||
const availableMinutes = this._cacheRange.timeSpanMinutes || 0;
|
||||
|
||||
return requestedMinutes > availableMinutes;
|
||||
}
|
||||
|
||||
private _formatCacheRange(): string {
|
||||
if (!this._cacheRange || this._cacheRange.totalCacheFolders === 0) {
|
||||
return 'No cache available';
|
||||
}
|
||||
|
||||
const oldest = this._cacheRange.oldestCache?.cacheTimestamp;
|
||||
const newest = this._cacheRange.newestCache?.cacheTimestamp;
|
||||
const timeSpan = this._cacheRange.timeSpanMinutes;
|
||||
|
||||
if (oldest && newest) {
|
||||
const oldestDate = new Date(oldest);
|
||||
const newestDate = new Date(newest);
|
||||
|
||||
// Use local timezone for display
|
||||
const locale = this.hass?.locale?.language || 'en-AU';
|
||||
const timeZone = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
const oldestStr = oldestDate.toLocaleString(locale, {
|
||||
timeZone: timeZone,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
const newestStr = newestDate.toLocaleString(locale, {
|
||||
timeZone: timeZone,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
if (timeSpan) {
|
||||
const hours = Math.floor(timeSpan / 60);
|
||||
const minutes = timeSpan % 60;
|
||||
return `${oldestStr} to ${newestStr} (${hours}h ${minutes}m, ${this._cacheRange.totalCacheFolders} folders)`;
|
||||
}
|
||||
return `${oldestStr} to ${newestStr} (${this._cacheRange.totalCacheFolders} folders)`;
|
||||
}
|
||||
|
||||
return `${this._cacheRange.totalCacheFolders} cache folders available`;
|
||||
}
|
||||
|
||||
private _validateCustomRange(): string | null {
|
||||
// Only validate if timespan is custom
|
||||
if (this._config.timespan !== 'custom') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this._config.custom_start_time || !this._config.custom_end_time) {
|
||||
return null; // No validation needed if fields are empty
|
||||
}
|
||||
|
||||
try {
|
||||
const startTime = new Date(this._config.custom_start_time);
|
||||
const endTime = new Date(this._config.custom_end_time);
|
||||
|
||||
if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) {
|
||||
return 'Invalid date format. Use ISO 8601 format (e.g., 2025-01-15T10:00:00Z)';
|
||||
}
|
||||
|
||||
if (startTime >= endTime) {
|
||||
return 'Start time must be before end time';
|
||||
}
|
||||
|
||||
if (this._cacheRange && this._cacheRange.oldestCache && this._cacheRange.newestCache) {
|
||||
try {
|
||||
const oldestCache = new Date(this._cacheRange.oldestCache.cacheTimestamp);
|
||||
const newestCache = new Date(this._cacheRange.newestCache.cacheTimestamp);
|
||||
|
||||
if (isNaN(oldestCache.getTime()) || isNaN(newestCache.getTime())) {
|
||||
return null; // Can't validate if cache dates are invalid
|
||||
}
|
||||
|
||||
if (startTime < oldestCache) {
|
||||
const locale = this.hass?.locale?.language || 'en-AU';
|
||||
const timeZone = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const oldestStr = oldestCache.toLocaleString(locale, {
|
||||
timeZone: timeZone,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
return `Start time is before available cache (${oldestStr})`;
|
||||
}
|
||||
|
||||
if (endTime > newestCache) {
|
||||
const locale = this.hass?.locale?.language || 'en-AU';
|
||||
const timeZone = this.hass?.config?.time_zone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const newestStr = newestCache.toLocaleString(locale, {
|
||||
timeZone: timeZone,
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
return `End time is after available cache (${newestStr})`;
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore cache date parsing errors
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Valid
|
||||
} catch (err) {
|
||||
return 'Invalid date format';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+7
-5
@@ -116,7 +116,6 @@ export interface BomLocalRadarCardConfig extends LovelaceCardConfig {
|
||||
// Display
|
||||
show_card_title?: boolean; // Show/hide card title (uses HA card header)
|
||||
card_title?: string;
|
||||
show_timestamp?: boolean; // Deprecated - use show_metadata instead
|
||||
show_metadata?: boolean | MetadataDisplayConfig; // Granular metadata display
|
||||
|
||||
// Control visibility
|
||||
@@ -126,10 +125,13 @@ export interface BomLocalRadarCardConfig extends LovelaceCardConfig {
|
||||
image_zoom?: number; // 1.0 = 100%, 1.5 = 150%, etc. (0.5 to 3.0)
|
||||
image_fit?: 'contain' | 'cover' | 'fill';
|
||||
|
||||
// Overlay options
|
||||
overlay_controls?: boolean; // Overlay controls on image
|
||||
overlay_position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
overlay_opacity?: number; // 0.0 to 1.0
|
||||
// Controls overlay options (separate from metadata overlay)
|
||||
controls_overlay_position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
controls_overlay_opacity?: number; // 0.0 to 1.0
|
||||
|
||||
// Metadata overlay options (separate from controls overlay)
|
||||
metadata_overlay_position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
metadata_overlay_opacity?: number; // 0.0 to 1.0
|
||||
|
||||
// Slideshow configuration
|
||||
timespan?: 'latest' | '1h' | '3h' | '6h' | '12h' | '24h' | 'custom'; // Historical data timespan
|
||||
|
||||
Reference in New Issue
Block a user