diff --git a/server/CLAUDE.md b/server/CLAUDE.md index 8fb351b..8656bbe 100644 --- a/server/CLAUDE.md +++ b/server/CLAUDE.md @@ -130,6 +130,31 @@ After restarting the server with new code: ## Frontend UI Patterns +### Entity Cards + +All entity cards (devices, targets, CSS sources, streams, scenes, automations, etc.) **must support clone functionality**. Clone buttons use the `ICON_CLONE` (📋) icon in `.card-actions`. + +**Clone pattern**: Clone must open the entity's add/create modal with fields prefilled from the cloned item. It must **never** silently create a duplicate — the user should review and confirm. + +Implementation: + +1. Export a `cloneMyEntity(id)` function that fetches (or finds in cache) the entity data +2. Call the add/create modal function, passing the entity data as `cloneData` +3. In the modal opener, detect clone mode (no ID + cloneData present) and prefill all fields +4. Append `' (Copy)'` to the name +5. Set the modal title to the "add" variant (not "edit") +6. The save action creates a new entity (POST), not an update (PUT) + +```javascript +export async function cloneMyEntity(id) { + const entity = myCache.data.find(e => e.id === id); + if (!entity) return; + showMyEditor(null, entity); // null id = create mode, entity = cloneData +} +``` + +Register the clone function in `app.js` window exports so inline `onclick` handlers can call it. + ### Modal Dialogs **IMPORTANT**: All modal dialogs must follow these standards for consistent UX: diff --git a/server/src/wled_controller/api/routes/scene_presets.py b/server/src/wled_controller/api/routes/scene_presets.py index 9767d75..8e1493d 100644 --- a/server/src/wled_controller/api/routes/scene_presets.py +++ b/server/src/wled_controller/api/routes/scene_presets.py @@ -164,27 +164,6 @@ async def delete_scene_preset( raise HTTPException(status_code=404, detail=str(e)) -# ===== Clone ===== - -@router.post( - "/api/v1/scene-presets/{preset_id}/clone", - response_model=ScenePresetResponse, - tags=["Scene Presets"], - status_code=201, -) -async def clone_scene_preset( - preset_id: str, - _auth: AuthRequired, - store: ScenePresetStore = Depends(get_scene_preset_store), -): - """Duplicate a scene preset with all its stored target snapshots.""" - try: - cloned = store.clone_preset(preset_id) - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) - - return _preset_to_response(cloned) - # ===== Recapture ===== diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index a01c765..f031b29 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -80,7 +80,7 @@ import { import { loadAutomations, openAutomationEditor, closeAutomationEditorModal, saveAutomationEditor, addAutomationCondition, - toggleAutomationEnabled, deleteAutomation, copyWebhookUrl, + toggleAutomationEnabled, cloneAutomation, deleteAutomation, copyWebhookUrl, expandAllAutomationSections, collapseAllAutomationSections, } from './features/automations.js'; import { @@ -315,6 +315,7 @@ Object.assign(window, { saveAutomationEditor, addAutomationCondition, toggleAutomationEnabled, + cloneAutomation, deleteAutomation, copyWebhookUrl, expandAllAutomationSections, diff --git a/server/src/wled_controller/static/js/features/automations.js b/server/src/wled_controller/static/js/features/automations.js index 9627971..857457d 100644 --- a/server/src/wled_controller/static/js/features/automations.js +++ b/server/src/wled_controller/static/js/features/automations.js @@ -185,6 +185,7 @@ function createAutomationCard(automation, sceneMap = new Map()) {
${condPills}
`, actions: ` + `; + targetList.appendChild(item); + } + _refreshTargetSelect(); + } + } catch { /* ignore */ } } + + scenePresetModal.open(); + scenePresetModal.snapshot(); } // ===== Delete ===== diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 62b20b8..2b9b670 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -652,6 +652,7 @@ "automations.created": "Automation created", "automations.deleted": "Automation deleted", "automations.error.name_required": "Name is required", + "automations.error.clone_failed": "Failed to clone automation", "scenes.title": "Scenes", "scenes.add": "Capture Scene", "scenes.edit": "Edit Scene", @@ -1172,7 +1173,7 @@ "sync_clock.name.placeholder": "Main Animation Clock", "sync_clock.name.hint": "A descriptive name for this synchronization clock", "sync_clock.speed": "Speed:", - "sync_clock.speed.hint": "Speed multiplier shared by all linked sources. 1.0 = normal speed.", + "sync_clock.speed.hint": "Animation speed multiplier for all linked sources. 1.0 = normal, 2.0 = double speed, 0.5 = half speed.", "sync_clock.description": "Description (optional):", "sync_clock.description.placeholder": "Optional description", "sync_clock.description.hint": "Optional notes about this clock's purpose", @@ -1189,7 +1190,7 @@ "sync_clock.paused": "Clock paused", "sync_clock.resumed": "Clock resumed", "sync_clock.reset_done": "Clock reset to zero", - "sync_clock.delete.confirm": "Delete this sync clock? Sources using it will revert to their own speed.", + "sync_clock.delete.confirm": "Delete this sync clock? Linked sources will lose synchronization and run at default speed.", "color_strip.clock": "Sync Clock:", - "color_strip.clock.hint": "Link to a sync clock for synchronized animation. When set, speed comes from the clock." + "color_strip.clock.hint": "Link to a sync clock to synchronize animation timing across sources. Speed is controlled on the clock." } diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index e515256..c3baf94 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -652,6 +652,7 @@ "automations.created": "Автоматизация создана", "automations.deleted": "Автоматизация удалена", "automations.error.name_required": "Введите название", + "automations.error.clone_failed": "Не удалось клонировать автоматизацию", "scenes.title": "Сцены", "scenes.add": "Захватить сцену", "scenes.edit": "Редактировать сцену", @@ -1172,7 +1173,7 @@ "sync_clock.name.placeholder": "Основные часы анимации", "sync_clock.name.hint": "Описательное название для этих часов синхронизации", "sync_clock.speed": "Скорость:", - "sync_clock.speed.hint": "Множитель скорости, общий для всех привязанных источников. 1.0 = нормальная скорость.", + "sync_clock.speed.hint": "Множитель скорости анимации для всех привязанных источников. 1.0 = обычная, 2.0 = двойная, 0.5 = половинная.", "sync_clock.description": "Описание (необязательно):", "sync_clock.description.placeholder": "Необязательное описание", "sync_clock.description.hint": "Необязательные заметки о назначении этих часов", @@ -1189,7 +1190,7 @@ "sync_clock.paused": "Часы приостановлены", "sync_clock.resumed": "Часы возобновлены", "sync_clock.reset_done": "Часы сброшены на ноль", - "sync_clock.delete.confirm": "Удалить эти часы синхронизации? Источники, использующие их, вернутся к собственной скорости.", + "sync_clock.delete.confirm": "Удалить эти часы синхронизации? Привязанные источники потеряют синхронизацию и будут работать на скорости по умолчанию.", "color_strip.clock": "Часы синхронизации:", - "color_strip.clock.hint": "Привязка к часам синхронизации для синхронной анимации. При установке скорость берётся из часов." + "color_strip.clock.hint": "Привязка к часам для синхронизации анимации между источниками. Скорость управляется на часах." } diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index a8a353b..4dc65d8 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -652,6 +652,7 @@ "automations.created": "自动化已创建", "automations.deleted": "自动化已删除", "automations.error.name_required": "名称为必填项", + "automations.error.clone_failed": "克隆自动化失败", "scenes.title": "场景", "scenes.add": "捕获场景", "scenes.edit": "编辑场景", @@ -1172,7 +1173,7 @@ "sync_clock.name.placeholder": "主动画时钟", "sync_clock.name.hint": "此同步时钟的描述性名称", "sync_clock.speed": "速度:", - "sync_clock.speed.hint": "所有关联源共享的速度倍率。1.0 = 正常速度。", + "sync_clock.speed.hint": "所有关联源的动画速度倍率。1.0 = 正常,2.0 = 双倍,0.5 = 半速。", "sync_clock.description": "描述(可选):", "sync_clock.description.placeholder": "可选描述", "sync_clock.description.hint": "关于此时钟用途的可选备注", @@ -1189,7 +1190,7 @@ "sync_clock.paused": "时钟已暂停", "sync_clock.resumed": "时钟已恢复", "sync_clock.reset_done": "时钟已重置为零", - "sync_clock.delete.confirm": "删除此同步时钟?使用它的源将恢复为各自的速度。", + "sync_clock.delete.confirm": "删除此同步时钟?关联的源将失去同步并以默认速度运行。", "color_strip.clock": "同步时钟:", - "color_strip.clock.hint": "关联同步时钟以实现同步动画。设置后,速度将来自时钟。" + "color_strip.clock.hint": "关联同步时钟以在多个源之间同步动画。速度在时钟上控制。" } diff --git a/server/src/wled_controller/storage/scene_preset_store.py b/server/src/wled_controller/storage/scene_preset_store.py index ac80868..e4ae532 100644 --- a/server/src/wled_controller/storage/scene_preset_store.py +++ b/server/src/wled_controller/storage/scene_preset_store.py @@ -117,25 +117,6 @@ class ScenePresetStore: logger.info(f"Recaptured scene preset: {preset_id}") return existing - def clone_preset(self, preset_id: str) -> ScenePreset: - """Duplicate an existing preset with a new ID and '(Copy)' suffix.""" - if preset_id not in self._presets: - raise ValueError(f"Scene preset not found: {preset_id}") - - source = self._presets[preset_id] - new_id = f"scene_{uuid.uuid4().hex[:8]}" - cloned = ScenePreset( - id=new_id, - name=f"{source.name} (Copy)", - description=source.description, - targets=[TargetSnapshot.from_dict(t.to_dict()) for t in source.targets], - order=source.order, - ) - self._presets[new_id] = cloned - self._save() - logger.info(f"Cloned scene preset: {preset_id} -> {new_id}") - return cloned - def delete_preset(self, preset_id: str) -> None: if preset_id not in self._presets: raise ValueError(f"Scene preset not found: {preset_id}")