diff --git a/xiaoshi-device-todo-button.js b/xiaoshi-device-todo-button.js
deleted file mode 100644
index 52ea3cf..0000000
--- a/xiaoshi-device-todo-button.js
+++ /dev/null
@@ -1,2024 +0,0 @@
-import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module";
-
-class XiaoshiTodoButtonEditor extends LitElement {
- static get properties() {
- return {
- hass: { type: Object },
- config: { type: Object },
- _searchTerm: { type: String },
- _filteredEntities: { type: Array },
- _showEntityList: { type: Boolean }
- };
- }
-
- static get styles() {
- return css`
- .form {
- display: flex;
- flex-direction: column;
- gap: 10px;
- min-height: 500px;
- }
- .form-group {
- display: flex;
- flex-direction: column;
- gap: 5px;
- }
- label {
- font-weight: bold;
- }
- select, input, textarea {
- padding: 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- }
- textarea {
- min-height: 80px;
- resize: vertical;
- }
- .help-text {
- font-size: 0.85em;
- color: #666;
- margin-top: 4px;
- }
-
- .entity-selector {
- position: relative;
- }
-
- .entity-search-input {
- width: 100%;
- padding: 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- box-sizing: border-box;
- }
-
- .entity-dropdown {
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- height: 300px;
- overflow-y: auto;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- z-index: 1000;
- margin-top: 2px;
- }
-
- .entity-option {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 12px;
- cursor: pointer;
- border-bottom: 1px solid #eee;
- }
-
- .entity-option:hover {
- background: #f5f5f5;
- }
-
- .entity-option.selected {
- background: #e3f2fd;
- }
-
- .entity-info {
- display: flex;
- align-items: center;
- gap: 8px;
- flex: 1;
- }
-
- .entity-details {
- flex: 1;
- }
-
- .entity-name {
- font-weight: 500;
- font-size: 14px;
- color: #000;
- }
-
- .entity-id {
- font-size: 12px;
- color: #000;
- font-family: monospace;
- }
-
- .check-icon {
- color: #4CAF50;
- }
-
- .no-results {
- padding: 12px;
- text-align: center;
- color: #666;
- font-style: italic;
- }
-
- .selected-entities {
- margin-top: 8px;
- }
-
- .selected-label {
- font-size: 12px;
- font-weight: bold;
- margin-bottom: 4px;
- color: #333;
- }
-
- .selected-entity {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- background: #f0f0f0;
- padding: 4px 8px;
- border-radius: 16px;
- margin: 2px 4px 2px 0;
- font-size: 12px;
- color: #000;
- }
-
- .remove-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 0;
- display: flex;
- align-items: center;
- color: #f44336;
- }
-
- .remove-btn:hover {
- color: #d32f2f;
- }
-
- /*button新元素 开始*/
- .checkbox-group {
- display: flex;
- align-items: center;
- gap: 0;
- margin: 0;
- padding: 0;
- }
-
- .checkbox-input {
- margin: 0;
- }
-
- .checkbox-label {
- font-weight: normal;
- margin: 0;
- }
- /*button新元素 结束*/
- `;
- }
-
- render() {
- if (!this.hass) return html``;
-
- return html`
-
-
- `;
- }
-
- _entityChanged(e) {
-
- /*button新按钮方法 开始*/
- const { name, value, type, checked } = e.target;
-
- let finalValue;
- // 处理复选框
- if (type === 'checkbox') {
- finalValue = checked;
- } else {
- if (!value && name !== 'theme' && name !== 'button_width' && name !== 'button_height' && name !== 'button_font_size' && name !== 'button_icon_size' && name !== 'width' && name !== 'tap_action') return;
- finalValue = value
- }
-
- // 处理不同字段的默认值
- if (name === 'button_width') {
- finalValue = value || '100%';
- } else if (name === 'button_height') {
- finalValue = value || '24px';
- } else if (name === 'button_font_size') {
- finalValue = value || '11px';
- } else if (name === 'button_icon_size') {
- finalValue = value || '13px';
- } else if (name === 'width') {
- finalValue = value || '100%';
- } else if (name === 'tap_action') {
- // 处理 tap_action 的特殊逻辑
- if (value === 'tap_action') {
- // 如果是弹出卡片,则不设置 tap_action,让组件使用默认逻辑
- finalValue = undefined;
- } else {
- finalValue = value;
- }
- }
- /*button新按钮方法 结束*/
-
- this.config = {
- ...this.config,
- [name]: finalValue
- };
-
- this.dispatchEvent(new CustomEvent('config-changed', {
- detail: { config: this.config },
- bubbles: true,
- composed: true
- }));
- }
-
- _onEntitySearch(e) {
- const searchTerm = e.target.value.toLowerCase();
- this._searchTerm = searchTerm;
- this._showEntityList = true;
-
- if (!this.hass) return;
-
- // 获取所有实体
- const allEntities = Object.values(this.hass.states);
-
- // 过滤实体,默认只显示todo.开头的实体
- this._filteredEntities = allEntities.filter(entity => {
- const entityId = entity.entity_id.toLowerCase();
- const friendlyName = (entity.attributes.friendly_name || '').toLowerCase();
-
- // 默认只显示todo.开头的实体,或者搜索时匹配搜索词
- const isTodoEntity = entityId.startsWith('todo.');
- const matchesSearch = entityId.includes(searchTerm) || friendlyName.includes(searchTerm);
-
- return isTodoEntity && matchesSearch;
- }).slice(0, 50); // 限制显示数量
-
- this.requestUpdate();
- }
-
- _toggleEntity(entityId) {
- const currentEntities = this.config.entities || [];
- let newEntities;
-
- if (currentEntities.includes(entityId)) {
- // 移除实体
- newEntities = currentEntities.filter(id => id !== entityId);
- } else {
- // 添加实体
- newEntities = [...currentEntities, entityId];
- }
-
- this.config = {
- ...this.config,
- entities: newEntities
- };
-
- this.dispatchEvent(new CustomEvent('config-changed', {
- detail: { config: this.config },
- bubbles: true,
- composed: true
- }));
-
- this.requestUpdate();
- }
-
- _removeEntity(entityId) {
- const currentEntities = this.config.entities || [];
- const newEntities = currentEntities.filter(id => id !== entityId);
-
- this.config = {
- ...this.config,
- entities: newEntities
- };
-
- this.dispatchEvent(new CustomEvent('config-changed', {
- detail: { config: this.config },
- bubbles: true,
- composed: true
- }));
-
- this.requestUpdate();
- }
-
- // 点击外部关闭下拉列表
- firstUpdated() {
- document.addEventListener('click', (e) => {
- if (!e.target.closest('.entity-selector')) {
- this._showEntityList = false;
- this.requestUpdate();
- }
- });
- }
-
- constructor() {
- super();
- this._searchTerm = '';
- this._filteredEntities = [];
- this._showEntityList = false;
- }
-
- setConfig(config) {
- this.config = config;
- }
-}
-customElements.define('xiaoshi-todo-button-editor', XiaoshiTodoButtonEditor);
-
-class XiaoshiTodoButton extends LitElement {
- static get properties() {
- return {
- hass: Object,
- config: Object,
- _todoData: Array,
- _loading: Boolean,
- _refreshInterval: Number,
- _dataLoaded: Boolean, //button新元素
- theme: { type: String },
- _editingItem: { type: Object },
- _expandedAddForm: { type: Object }
- };
- }
-
- static get styles() {
- return css`
- :host {
- display: block;
- width: var(--card-width, 100%);
- }
-
- /*button新元素 开始*/
- .todo-status {
- width: var(--button-width, 70px);
- height: var(--button-height, 24px);
- padding: 0;
- margin: 0;
- background: var(--bg-color, #fff);
- color: var(--fg-color, #000);
- border-radius: 10px;
- font-size: var(--button-font-size, 11px);
- font-weight: 500;
- text-align: center;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0;
- cursor: pointer;
- transition: background-color 0.2s, transform 0.1s;
- position: relative;
- }
-
- .status-icon {
- --mdc-icon-size: var(--button-icon-size, 13px);
- color: var(--fg-color, #000);
- margin-right: 3px;
- }
-
- /* 角标模式样式 */
- .todo-status.badge-mode {
- width: var(--button-width, 70px);
- height: var(--button-height, 24px);
- border-radius: 10px;
- padding: 0;
- margin: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .todo-status.badge-mode .status-icon {
- color: rgb(128, 128, 128);
- transition: color 0.2s;
- }
-
- .todo-status.badge-mode.has-warning .status-icon {
- color: rgb(255, 0, 0);
- }
-
- .badge-number {
- position: absolute;
- top: -6px;
- right: -6px;
- min-width: 12px;
- height: 12px;
- background: rgb(255, 0, 0);
- color: rgb(255, 255, 255);
- border-radius: 50%;
- font-size: 8px;
- font-weight: bold;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- box-sizing: border-box;
- line-height: 1;
- }
-
- /*button新元素 结束*/
-
- ha-card {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- background: var(--bg-color, #fff);
- border-radius: 12px;
- }
-
- /*标题容器*/
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px;
- background: var(--bg-color, #fff);
- border-radius: 12px;
- }
-
- /*标题红色圆点*/
- .offline-indicator {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- margin-right: 8px;
- background: rgb(255, 165, 0);
- }
-
- /*标题红色圆点动画*/
- @keyframes pulse {
- 0% { opacity: 1; }
- 50% { opacity: 0.5; }
- 100% { opacity: 1; }
- }
-
- /*标题*/
- .card-title {
- font-size: 20px;
- font-weight: 500;
- color: var(--fg-color, #000);
- height: 30px;
- line-height: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
-
- }
-
- /*标题统计数字*/
- .device-count {
- color: var(--fg-color, #000);
- border-radius: 8px;
- font-size: 13px;
- width: 30px;
- height: 30px;
- text-align: center;
- line-height: 30px;
- font-weight: bold;
- padding: 0px;
- background: rgb(255, 165, 0);
- }
-
- /*标题刷新按钮*/
- .refresh-btn {
- color: var(--fg-color, #fff);
- border: none;
- border-radius: 8px;
- padding: 5px;
- cursor: pointer;
- font-size: 13px;
- width: 50px;
- height: 30px;
- line-height: 30px;
- text-align: center;
- font-weight: bold;
- padding: 0px;
- background: rgb(255, 165, 0);
- }
-
- /*2级标题*/
- .section-divider {
- margin: 0 0 8px 0;
- padding: 8px 8px;
- background: var(--bg-color, #fff);
- font-weight: 500;
- color: var(--fg-color, #000);
- border-top: 1px solid rgb(150,150,150,0.5);
- border-bottom: 1px solid rgb(150,150,150,0.5);
- margin: 0 16px 0 16px;
-
- }
-
- /*2级标题字体*/
- .section-title {
- display: flex;
- align-items: center;
- justify-content: space-between;
- color: var(--fg-color, #000);
- font-size: 13px;
- }
-
- /*2级标题,统计数量字体*/
- .section-count {
- background: rgb(255,165,0);
- color: var(--fg-color, #000);
- border-radius: 12px;
- width: 15px;
- height: 15px;
- text-align: center;
- line-height: 15px;
- padding: 3px;
- font-size: 12px;
- font-weight: bold;
- }
-
- /*设备、实体明细*/
- .device-item {
- display: flex;
- align-items: center;
- padding: 0px;
- border-bottom: 1px solid rgb(150,150,150,0.2);
- margin: 0 32px 0px 32px;
- }
-
- /*设备、实体明细背景*/
- .devices-list {
- flex: 1;
- overflow-y: auto;
- min-height: 0;
- padding: 0 0 8px 0;
- }
-
- .device-icon {
- margin-right: 12px;
- color: var(--error-color);
- }
-
- .device-info {
- flex-grow: 1;
- padding: 6px 0;
- }
-
- .device-name {
- font-weight: 500;
- color: var(--fg-color, #000);
- padding: 6px 0 0 0;
- }
-
- .device-entity {
- font-size: 10px;
- color: var(--fg-color, #000);
- font-family: monospace;
- }
-
- .device-details {
- font-size: 10px;
- color: var(--fg-color, #000);
- }
-
- .device-last-seen {
- font-size: 10px;
- color: var(--fg-color, #000);
- margin-left: auto;
- }
-
- .no-devices {
- text-align: center;
- padding: 8px 0 0 0;
- color: var(--fg-color, #000);
- }
-
- .loading {
- text-align: center;
- padding: 0px;
- color: var(--fg-color, #000);
- }
-
- /*加油图标样式*/
- .device-details ha-icon {
- --mdc-icon-size: 12px;
- color: var(--fg-color, #000);
- }
-
- /*待办事项样式*/
- .todo-item {
- transition: background-color 0.2s ease;
- }
-
- .todo-item:hover {
- background-color: rgba(150,150,150,0.1);
- border-radius: 4px;
- }
-
- .todo-item input[type="checkbox"] {
- cursor: pointer;
- }
-
- .todo-item button {
- background: none;
- border: none;
- cursor: pointer;
- padding: 4px;
- border-radius: 4px;
- transition: background-color 0.2s ease;
- }
-
- .todo-item button {
- color: #f44336;
- }
-
- .todo-item button:hover {
- background-color: rgba(244, 67, 54, 0.1);
- color: #d32f2f;
- }
-
- .add-todo {
- display: flex;
- gap: 4px;
- margin-top: 8px;
- }
-
- .add-todo input {
- flex: 1;
- padding: 4px;
- border-radius: 4px;
- background: var(--bg-color, #fff);
- border: 1px solid var(--fg-color, #000);
- color: var(--fg-color, #000);
- }
-
- .add-todo button {
- padding: 4px 8px;
- border-radius: 4px;
- border: 1px solid var(--fg-color, #000);
- background: var(--bg-color, #fff);
- color: var(--fg-color, #000);
- cursor: pointer;
- }
-
- .add-todo input:focus {
- outline: none;
- border-color: #2196F3;
- box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
- }
-
- .add-todo-expanded {
- display: flex;
- flex-direction: column;
- gap: 8px;
- margin-top: 8px;
- padding: 8px;
- border: 1px solid var(--fg-color, #000);
- border-radius: 4px;
- background: var(--bg-color, #fff);
- }
-
- .add-todo-row {
- display: flex;
- gap: 8px;
- align-items: center;
- }
-
- .add-todo-description {
- flex: 1;
- padding: 4px;
- border: 1px solid var(--fg-color, #000);
- border-radius: 4px;
- background: var(--bg-color, #fff);
- color: var(--fg-color, #000);
- font-size: 13px;
- }
-
- .add-todo-description:focus {
- outline: none;
- border-color: #2196F3;
- box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
- }
-
- .add-todo-date {
- padding: 4px;
- border: 1px solid var(--fg-color, #000);
- border-radius: 4px;
- background: var(--bg-color, #fff);
- color: var(--fg-color, #000);
- font-size: 12px;
- width: 120px;
- }
-
- /* 确保日期输入框显示正确的格式 */
- input[type="date"] {
- color-scheme: light dark;
- }
-
- input[type="date"]::-webkit-calendar-picker-indicator {
- cursor: pointer;
- filter: invert(0.5);
- }
-
- /* 深色主题下的日期选择器 */
- [theme="off"] input[type="date"]::-webkit-calendar-picker-indicator {
- filter: invert(1);
- }
-
- .add-todo-toggle {
- background: none;
- border: 1px solid var(--fg-color, #000);
- color: var(--fg-color, #000);
- padding: 4px 8px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- margin-top: 8px;
- margin-bottom: 2px;
- }
-
- .add-todo-toggle:hover {
- background-color: rgba(33, 150, 243, 0.1);
- border-color: #2196F3;
- }
-
- .todo-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- }
-
- .todo-main {
- display: flex;
- align-items: center;
- }
-
- .todo-due {
- color: #ff9800;
- font-size: 12px;
- margin-left: 4px;
- font-weight: 500;
- }
-
- .todo-description {
- color: #999;
- font-size: 11px;
- margin-top: 2px;
- line-height: 1.3;
- }
-
- .todo-item.no-description {
- align-items: center;
- }
-
- .todo-item.no-description input[type="checkbox"] {
- margin-top: 0;
- }
-
- .todo-item .edit-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 4px;
- border-radius: 4px;
- transition: background-color 0.2s ease;
- color: #2196F3 !important;
- margin-right: 4px;
- }
-
- .todo-item .edit-btn:hover {
- background-color: rgba(33, 150, 243, 0.1);
- color: #1976D2 !important;
- }
-
- .edit-input {
- flex: 1;
- padding: 4px;
- border: 1px solid var(--fg-color, #000);
- border-radius: 4px;
- background: var(--bg-color, #fff);
- color: var(--fg-color, #000);
- font-size: 13px;
- margin-right: 8px;
- }
-
- .edit-input:focus {
- outline: none;
- border-color: #2196F3;
- box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
- }
- `;
- }
-
- constructor() {
- super();
- this._todoData = [];
- this._loading = false;
- this._dataLoaded = false; //button新元素
- this._refreshInterval = null;
- this.theme = 'on';
- this._editingItem = null;
- this._expandedAddForm = {};
- }
-
- static getConfigElement() {
- return document.createElement("xiaoshi-todo-button-editor");
- }
-
- connectedCallback() {
- super.connectedCallback();
- this._loadTodoData();
-
-
- //button新元素 开始
- setTimeout(() => {
- this._loadTodoData();
- }, 50);
- //button新元素 结束
- // 设置主题属性
- this.setAttribute('theme', this._evaluateTheme());
-
- // 每300秒刷新一次数据,减少频繁刷新
- this._refreshInterval = setInterval(() => {
- this._loadTodoData();
- }, 300000);
- }
-
- _evaluateTheme() {
- try {
- if (!this.config || !this.config.theme) return 'on';
- if (typeof this.config.theme === 'function') {
- return this.config.theme();
- }
- if (typeof this.config.theme === 'string' &&
- (this.config.theme.includes('return') || this.config.theme.includes('=>'))) {
- return (new Function(`return ${this.config.theme}`))();
- }
- return this.config.theme;
- } catch(e) {
- console.error('计算主题时出错:', e);
- return 'on';
- }
- }
-
- _formatDate(dateString) {
- if (!dateString) return '';
-
- const date = new Date(dateString);
-
- // 检查日期是否有效
- if (isNaN(date.getTime())) {
- return dateString; // 如果无法解析,返回原字符串
- }
-
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
-
- return `${year}-${month}-${day}`;
- }
-
- _formatDateForInput(dateString) {
- if (!dateString) return '';
-
- const date = new Date(dateString);
-
- // 检查日期是否有效
- if (isNaN(date.getTime())) {
- return '';
- }
-
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
-
- return `${year}-${month}-${day}`;
- }
-
- _calculateDueDate(dueDate) {
- if (!dueDate) return '';
-
- const today = new Date();
- today.setHours(0, 0, 0, 0);
-
- const due = new Date(dueDate);
-
- // 检查日期是否有效
- if (isNaN(due.getTime())) {
- return dueDate; // 如果无法解析,返回原字符串
- }
-
- due.setHours(0, 0, 0, 0);
-
- const diffTime = due - today;
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
-
- if (diffDays === 0) {
- return '今天';
- } else if (diffDays === 1) {
- return '明天';
- } else if (diffDays === -1) {
- return '昨天';
- } else if (diffDays > 0 && diffDays <= 7) {
- return `${diffDays}天后`;
- } else if (diffDays > 7) {
- return this._formatDate(dueDate);
- } else {
- return `${Math.abs(diffDays)}天前`;
- }
- }
-
- disconnectedCallback() {
- super.disconnectedCallback();
- if (this._refreshInterval) {
- clearInterval(this._refreshInterval);
- }
- }
-
- async _loadTodoData() {
- if (!this.hass) return;
-
-
- // button新元素 开始 删除下面
- // this._loading = true;
- // this.requestUpdate();
- // button新元素 介素 删除下面
-
- try {
- const entities = this.config.entities || [];
- const todoData = [];
-
- for (const entityId of entities) {
- const entity = this.hass.states[entityId];
- if (!entity) continue;
-
- // 获取待办事项项目
- let todoItems = [];
- try {
- // 获取所有待办事项 - 使用 WebSocket API
- const result = await this.hass.callWS({
- type: 'todo/item/list',
- entity_id: entityId
- });
-
- todoItems = result.items || [];
- } catch (error) {
- console.error(`获取待办事项失败 ${entityId}:`, error);
- }
-
- const attributes = entity.attributes;
- todoData.push({
- entity_id: entityId,
- friendly_name: attributes.friendly_name || entityId,
- icon: attributes.icon || 'mdi:format-list-checks',
- state: entity.state || '0',
- items: todoItems,
- incomplete_count: todoItems.filter(item => item.status === 'needs_action').length,
- completed_count: todoItems.filter(item => item.status === 'completed').length
- });
- }
-
- this._todoData = todoData;
- this._dataLoaded = true; //button新元素
- } catch (error) {
- console.error('加载待办事项数据失败:', error);
- this._todoData = [];
- this._dataLoaded = true; //button新元素
- }
-
- this._loading = false;
- }
-
- _handleRefresh() {
- this._handleClick();
- this._loadTodoData();
- }
-
- _handleEntityClick(entity) {
- this._handleClick();
- // 点击实体时打开实体详情页
- if (entity.entity_id) {
- const evt = new Event('hass-more-info', { composed: true });
- evt.detail = { entityId: entity.entity_id };
- this.dispatchEvent(evt);
- }
- }
-
- _handleClick(){
- if (navigator.vibrate) {
- navigator.vibrate(50);
- }
- else if (navigator.webkitVibrate) {
- navigator.webkitVibrate(50);
- }
- else {
- }
- }
-
- /*button新元素 开始*/
- _handleButtonClick() {
- const tapAction = this.config.tap_action;
-
- if (!tapAction || tapAction !== 'none') {
- // 默认 tap_action 行为:弹出垂直堆叠卡片
- const excludedParams = ['type', 'button_height', 'button_width', 'button_font_size', 'button_icon_size', 'show_preview', 'tap_action'];
-
- // 构建垂直堆叠卡片的内容
- const cards = [];
-
- // 1. 添加待办信息卡片
- const todoCardConfig = {};
- Object.keys(this.config).forEach(key => {
- if (!excludedParams.includes(key) && key !== 'other_cards' && key !== 'no_preview') {
- todoCardConfig[key] = this.config[key];
- }
- });
-
- cards.push({
- type: 'custom:xiaoshi-todo-card',
- ...todoCardConfig
- });
-
- // 2. 添加附加卡片
- if (this.config.other_cards && this.config.other_cards.trim()) {
- try {
- const additionalCardsConfig = this._parseYamlCards(this.config.other_cards);
-
- // 为每个附加卡片传递 theme 值
- const cardsWithTheme = additionalCardsConfig.map(card => {
- // 如果卡片没有 theme 配置,则从当前卡片配置中传递
- if (!card.theme && this.config.theme) {
- return {
- ...card,
- theme: this.config.theme
- };
- }
- return card;
- });
-
- cards.push(...cardsWithTheme);
- } catch (error) {
- console.error('解析附加卡片配置失败:', error);
- }
- }
-
- // 创建垂直堆叠卡片
- const popupContent = {
- type: 'vertical-stack',
- cards: cards
- };
-
- const popupStyle = this.config.popup_style || `
- --mdc-theme-surface: rgb(0,0,0,0);
- --dialog-backdrop-filter: blur(10px) brightness(1);
- `;
-
- if (window.browser_mod) {
- window.browser_mod.service('popup', {
- style: popupStyle,
- content: popupContent
- });
- } else {
- console.warn('browser_mod not available, cannot show popup');
- }
- }
- this._handleClick();
- }
-
- _parseYamlCards(yamlString) {
- try {
- const lines = yamlString.split('\n');
- const cards = [];
- let currentCard = null;
- let indentStack = [];
- let contextStack = [];
-
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i];
- const trimmed = line.trim();
-
- if (!trimmed || trimmed.startsWith('#')) continue;
-
- const indentLevel = line.length - line.trimStart().length;
- if (trimmed.startsWith('- type')) {
- if (currentCard) {
- cards.push(currentCard);
- currentCard = null;
- indentStack = [];
- contextStack = [];
- }
- const content = trimmed.substring(1).trim();
- if (content.includes(':')) {
- const [key, ...valueParts] = content.split(':');
- const value = valueParts.join(':').trim();
- currentCard = {};
- this._setNestedValue(currentCard, key.trim(), this._parseValue(value));
- } else {
- currentCard = { type: content };
- }
-
- indentStack = [indentLevel];
- contextStack = [currentCard];
- } else if (currentCard && trimmed.startsWith('-')) {
- while (indentStack.length > 1 && indentLevel <= indentStack[indentStack.length - 1]) {
- indentStack.pop();
- contextStack.pop();
- }
-
- let currentContext = contextStack[contextStack.length - 1];
- const itemValue = trimmed.substring(1).trim();
-
- if (!Array.isArray(currentContext)) {
- if (contextStack.length > 1) {
- const parentContext = contextStack[contextStack.length - 2];
- for (let key in parentContext) {
- if (parentContext[key] === currentContext) {
- parentContext[key] = [];
- contextStack[contextStack.length - 1] = parentContext[key];
- currentContext = parentContext[key];
- break;
- }
- }
- }
- }
- if (Array.isArray(currentContext)) {
- if (itemValue.includes(':')) {
- const [key, ...valueParts] = itemValue.split(':');
- const value = valueParts.join(':').trim();
- const obj = {};
- obj[key.trim()] = this._parseValue(value);
- currentContext.push(obj);
- } else {
- currentContext.push(this._parseValue(itemValue));
- }
- }
- } else if (currentCard && trimmed.includes(':')) {
- const [key, ...valueParts] = trimmed.split(':');
- const value = valueParts.join(':').trim();
- const keyName = key.trim();
-
- while (indentStack.length > 1 && indentLevel <= indentStack[indentStack.length - 1]) {
- indentStack.pop();
- contextStack.pop();
- }
-
- const currentContext = contextStack[contextStack.length - 1];
-
- if (value) {
- this._setNestedValue(currentContext, keyName, this._parseValue(value));
- } else {
- let nextLine = null, nextIndent = null;
- for (let j = i + 1; j < lines.length; j++) {
- const nextTrimmed = lines[j].trim();
- if (nextTrimmed && !nextTrimmed.startsWith('#')) {
- nextLine = nextTrimmed;
- nextIndent = lines[j].length - lines[j].trimStart().length;
- break;
- }
- }
-
- currentContext[keyName] = (nextLine && nextLine.startsWith('-') && nextIndent > indentLevel)
- ? [] : (currentContext[keyName] || {});
-
- indentStack.push(indentLevel);
- contextStack.push(currentContext[keyName]);
- }
- }
- }
-
- if (currentCard) cards.push(currentCard);
-
- return cards;
- } catch (error) {
- console.error('YAML解析错误:', error);
- return [];
- }
- }
-
- _parseValue(value) {
- if (!value) return '';
-
- // 移除引号
- if ((value.startsWith('"') && value.endsWith('"')) ||
- (value.startsWith("'") && value.endsWith("'"))) {
- return value.slice(1, -1);
- }
-
- // 尝试解析为数字
- if (!isNaN(value) && value.trim() !== '') {
- return Number(value);
- }
-
- // 尝试解析为布尔值
- if (value === 'true') return true;
- if (value === 'false') return false;
- if (value === 'null') return null;
-
- // 返回字符串
- return value;
- }
-
- _setNestedValue(obj, path, value) {
- // 支持嵌套路径,如 "styles.card"
- const keys = path.split('.');
- let current = obj;
-
- for (let i = 0; i < keys.length - 1; i++) {
- const key = keys[i];
- if (!current[key] || typeof current[key] !== 'object') {
- current[key] = {};
- }
- current = current[key];
- }
-
- current[keys[keys.length - 1]] = value;
- }
-
- /*button新元素 结束*/
-
- async _addTodoItem(entityId, item, description = '', due = '') {
- try {
- const params = {
- entity_id: entityId,
- item: item
- };
-
- // 只有当描述不为空时才添加
- if (description && description.trim()) {
- params.description = description.trim();
- }
-
- // 只有当日期不为空时才添加
- if (due && due.trim()) {
- params.due_date = due.trim();
- }
-
- await this.hass.callService('todo', 'add_item', params);
- this._loadTodoData(); // 重新加载数据
- } catch (error) {
- console.error('添加待办事项失败:', error);
- }
- }
-
- async _removeTodoItem(entityId, item) {
- try {
- await this.hass.callService('todo', 'remove_item', {
- entity_id: entityId,
- item: item
- });
- this._loadTodoData(); // 重新加载数据
- } catch (error) {
- console.error('删除待办事项失败:', error);
- }
- }
-
- async _updateTodoItem(entityId, item, status) {
- try {
- await this.hass.callService('todo', 'update_item', {
- entity_id: entityId,
- item: item,
- status: status
- });
- this._loadTodoData(); // 重新加载数据
- } catch (error) {
- console.error('更新待办事项失败:', error);
- }
- }
-
- async _editTodoItem(entityId, oldItem, newItem, description = '', due = '') {
- try {
- // 先删除旧的待办事项,然后添加新的
- await this.hass.callService('todo', 'remove_item', {
- entity_id: entityId,
- item: oldItem
- });
-
- const params = {
- entity_id: entityId,
- item: newItem
- };
-
- // 只有当描述不为空时才添加
- if (description && description.trim()) {
- params.description = description.trim();
- }
-
- // 只有当日期不为空时才添加
- if (due && due.trim()) {
- params.due_date = due.trim();
- }
-
- await this.hass.callService('todo', 'add_item', params);
- this._loadTodoData(); // 重新加载数据
- } catch (error) {
- console.error('修改待办事项失败:', error);
- }
- }
-
- render() {
- if (!this.hass) {
- return html`等待Home Assistant连接...
`;
- }
- // 获取主题和颜色
- const theme = this._evaluateTheme();
- const fgColor = theme === 'on' ? 'rgb(0, 0, 0)' : 'rgb(255, 255, 255)';
- const bgColor = theme === 'on' ? 'rgb(255, 255, 255)' : 'rgb(50, 50, 50)';
-
- // 计算总待办数量
- const totalIncompleteCount = this._todoData.reduce((sum, todo) => sum + todo.incomplete_count, 0);
-
-
- /*button新元素 前9行和最后1行开始*/
- const showPreview = this.config.no_preview === true;
-
- // 获取新参数
- const badgeMode = this.config.badge_mode === true;
- const transparentBg = this.config.transparent_bg === true;
- const hideIcon = this.config.hide_icon === true;
- const hideColon = this.config.hide_colon === true;
- const hideZero = this.config.hide_zero === true;
- const autoHide = this.config.auto_hide === true;
- const buttonText = this.config.button_text || '待办';
- const buttonIcon = this.config.button_icon || 'mdi:clipboard-list';
-
- // 设置背景颜色
- const buttonBgColor = transparentBg ? 'transparent' : bgColor;
-
- // 检查是否需要自动隐藏(只有数据加载完成且数量为0时才考虑隐藏)
- const shouldAutoHide = this._dataLoaded && autoHide && totalIncompleteCount === 0;
-
- // 如果需要自动隐藏,返回空div
- if (shouldAutoHide) {
- return html``;
- }
-
- // 渲染按钮
- let buttonHtml;
- if (!this._dataLoaded) {
- if (badgeMode) {
- // 角标模式:只显示图标,数量为0时不显示角标
- buttonHtml = html`
-
-
-
- `;
- } else {
- // 普通模式
- // 构建显示文本
- let displayText = buttonText;
-
- // 根据hide_colon参数决定是否显示冒号
- if (!hideColon) {
- displayText += ':';
- } else {
- displayText += ' ';
- }
-
- // 根据hide_zero参数决定是否显示0值
- if (!hideZero) {
- displayText += ' 0';
- } else {
- // 隐藏0值时使用CSS空格占位符,保持布局稳定
- displayText += '\u2002'; // 两个en空格,大约等于数字"0"的宽度
- }
-
- buttonHtml = html`
-
- ${!hideIcon ? html`` : ''}
- ${displayText}
-
- `;
- }
- } else {
- // 数据加载完成后
- if (badgeMode) {
- // 角标模式:只显示图标,根据数量显示角标
- const hasWarning = totalIncompleteCount > 0;
- buttonHtml = html`
-
-
- ${hasWarning ? html`
${totalIncompleteCount}
` : ''}
-
- `;
- } else {
- // 普通模式:显示文本和数量
- const textColor = totalIncompleteCount === 0 ? fgColor : 'rgb(255, 0, 0)';
-
- // 构建显示文本
- let displayText = buttonText;
-
- // 根据hide_colon参数决定是否显示冒号
- if (!hideColon) {
- displayText += ':';
- } else {
- displayText += ' ';
- }
-
- // 根据hide_zero参数和实际数量决定是否显示数量
- if (hideZero && totalIncompleteCount === 0) {
- // 隐藏0值时使用CSS空格占位符,保持布局稳定
- displayText += '\u2002'; // 两个en空格,大约等于数字"0"的宽度
- } else {
- displayText += ` ${totalIncompleteCount}`;
- }
-
- buttonHtml = html`
-
- ${!hideIcon ? html`` : ''}
- ${displayText}
-
- `;
- }
- }
-
- // 返回最终的渲染结果(包括按钮和预览卡片)
- return html`
- ${buttonHtml}
- ${showPreview ? html`
-
-
-
-
-
-
-
-
- ${this._loading ?
- html`
加载中...
` :
-
- this._todoData.length === 0 ?
- html`
请配置待办事项实体
` :
- html`
- ${this._todoData.map(todoData => html`
-
-
- ${todoData.friendly_name}
- ${todoData.incomplete_count}
-
-
-
-
- ${todoData.items.length === 0 ?
- html`
暂无待办事项
` :
- html`
- ${(() => {
- // 将待办事项分为有时间和无时间两组
- const itemsWithoutTime = todoData.items.filter(item => !item.due);
- const itemsWithTime = todoData.items.filter(item => item.due);
-
- // 没有时间的按名称排序
- itemsWithoutTime.sort((a, b) => (a.summary || '').localeCompare(b.summary || ''));
-
- // 有时间的按时间排序
- itemsWithTime.sort((a, b) => {
- const dateA = new Date(a.due);
- const dateB = new Date(b.due);
- return dateA - dateB;
- });
-
- // 合并结果:无时间的在前,有时间的在后
- const sortedItems = [...itemsWithoutTime, ...itemsWithTime];
-
- return sortedItems.map(item => {
- const dueText = this._calculateDueDate(item.due);
- const isEditing = this._editingItem && this._editingItem.entityId === todoData.entity_id && this._editingItem.uid === item.uid;
-
- return html`
-
-
{
- this._updateTodoItem(todoData.entity_id, item.summary || item.uid, e.target.checked ? 'completed' : 'needs_action');
- this._handleClick();
- }}
- style="margin-right: 8px; margin-top: 2px;"
- />
- ${isEditing ? html`
-
- ` : html`
-
-
- ${item.summary}
- ${dueText ? html`(${dueText})` : ''}
-
- ${item.description ? html`
${item.description}
` : ''}
-
- `}
- ${!isEditing ? html`
-
- ` : ''}
-
-
- `;
- });
- })()}
- `
- }
-
-
-
- ${this._expandedAddForm[todoData.entity_id] ? html`
-
- ` : ''}
-
-
-
- `)}
- `
- }
-
-
- ` : html``}
- `;
- /*button新元素 结束*/
- }
-
- setConfig(config) {
- /*button新元素 开始*/
- // 不设置默认值,只有明确配置时才添加 no_preview
- this.config = {
- ...config
- };
- if (config.button_width) {
- this.style.setProperty('--button-width', config.button_width);
- } else {
- this.style.setProperty('--button-width', '70px');
- }
-
- // 设置按钮高度(只控制 todo-status)
- if (config.button_height) {
- this.style.setProperty('--button-height', config.button_height);
- } else {
- this.style.setProperty('--button-height', '24px');
- }
-
- // 设置按钮文字大小(只控制 todo-status)
- if (config.button_font_size) {
- this.style.setProperty('--button-font-size', config.button_font_size);
- } else {
- this.style.setProperty('--button-font-size', '11px');
- }
-
- // 设置按钮图标大小(只控制 todo-status)
- if (config.button_icon_size) {
- this.style.setProperty('--button-icon-size', config.button_icon_size);
- } else {
- this.style.setProperty('--button-icon-size', '13px');
- }
-
- // 设置卡片宽度(控制原来的 UI)
- if (config.width) {
- this.style.setProperty('--card-width', config.width);
- } else {
- this.style.setProperty('--card-width', '100%');
- }
- /*button新元素 结束*/
-
- // 设置主题
- if (config.theme) {
- this.setAttribute('theme', config.theme);
- }
- }
-
- getCardSize() {
- // 根据待办事项实体数量动态计算卡片大小
- const baseSize = 3;
- const entitySize = Math.max(0, Math.min(this._todoData.length * 2, 10));
- return baseSize + entitySize;
- }
-}
-customElements.define('xiaoshi-todo-button', XiaoshiTodoButton);