mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
feat: integrate Starlark/Tronbyte app support into plugin system
Add starlark-apps plugin that renders Tidbyt/Tronbyte .star apps via Pixlet binary and integrates them into the existing Plugin Manager UI as virtual plugins. Includes vegas scroll support, Tronbyte repository browsing, and per-app configuration. - Extract working starlark plugin code from starlark branch onto fresh main - Fix plugin conventions (get_logger, VegasDisplayMode, BasePlugin) - Add 13 starlark API endpoints to api_v3.py (CRUD, browse, install, render) - Virtual plugin entries (starlark:<app_id>) in installed plugins list - Starlark-aware toggle and config routing in pages_v3.py - Tronbyte repository browser section in Plugin Store UI - Pixlet binary download script (scripts/download_pixlet.sh) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1852,6 +1852,31 @@ def get_installed_plugins():
|
||||
'vegas_content_type': vegas_content_type
|
||||
})
|
||||
|
||||
# Append virtual entries for installed Starlark apps
|
||||
starlark_plugin = api_v3.plugin_manager.get_plugin('starlark-apps')
|
||||
if starlark_plugin and hasattr(starlark_plugin, 'apps'):
|
||||
for app_id, app in starlark_plugin.apps.items():
|
||||
plugins.append({
|
||||
'id': f'starlark:{app_id}',
|
||||
'name': app.manifest.get('name', app_id),
|
||||
'version': 'starlark',
|
||||
'author': app.manifest.get('author', 'Tronbyte Community'),
|
||||
'category': 'Starlark App',
|
||||
'description': app.manifest.get('summary', 'Starlark app'),
|
||||
'tags': ['starlark'],
|
||||
'enabled': app.is_enabled(),
|
||||
'verified': False,
|
||||
'loaded': True,
|
||||
'last_updated': None,
|
||||
'last_commit': None,
|
||||
'last_commit_message': None,
|
||||
'branch': None,
|
||||
'web_ui_actions': [],
|
||||
'vegas_mode': 'fixed',
|
||||
'vegas_content_type': 'multi',
|
||||
'is_starlark_app': True,
|
||||
})
|
||||
|
||||
return jsonify({'status': 'success', 'data': {'plugins': plugins}})
|
||||
except Exception as e:
|
||||
import traceback
|
||||
@@ -2127,6 +2152,20 @@ def toggle_plugin():
|
||||
current_enabled = config.get(plugin_id, {}).get('enabled', False)
|
||||
enabled = not current_enabled
|
||||
|
||||
# Handle starlark app toggle (starlark:<app_id> prefix)
|
||||
if plugin_id.startswith('starlark:'):
|
||||
starlark_app_id = plugin_id[len('starlark:'):]
|
||||
starlark_plugin = api_v3.plugin_manager.get_plugin('starlark-apps')
|
||||
if not starlark_plugin or starlark_app_id not in starlark_plugin.apps:
|
||||
return jsonify({'status': 'error', 'message': f'Starlark app not found: {starlark_app_id}'}), 404
|
||||
app = starlark_plugin.apps[starlark_app_id]
|
||||
app.manifest['enabled'] = enabled
|
||||
with open(starlark_plugin.manifest_file, 'r') as f:
|
||||
manifest = json.load(f)
|
||||
manifest['apps'][starlark_app_id]['enabled'] = enabled
|
||||
starlark_plugin._save_manifest(manifest)
|
||||
return jsonify({'status': 'success', 'message': f"Starlark app {'enabled' if enabled else 'disabled'}", 'enabled': enabled})
|
||||
|
||||
# Check if plugin exists in manifests (discovered but may not be loaded)
|
||||
if plugin_id not in api_v3.plugin_manager.plugin_manifests:
|
||||
return jsonify({'status': 'error', 'message': f'Plugin {plugin_id} not found'}), 404
|
||||
@@ -6903,4 +6942,525 @@ def clear_old_errors():
|
||||
message="Failed to clear old errors",
|
||||
details=str(e),
|
||||
status_code=500
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ─── Starlark Apps API ──────────────────────────────────────────────────────
|
||||
|
||||
def _get_tronbyte_repository_class():
|
||||
"""Import TronbyteRepository from plugin-repos directory."""
|
||||
import importlib.util
|
||||
|
||||
module_path = PROJECT_ROOT / 'plugin-repos' / 'starlark-apps' / 'tronbyte_repository.py'
|
||||
if not module_path.exists():
|
||||
raise ImportError(f"TronbyteRepository module not found at {module_path}")
|
||||
|
||||
spec = importlib.util.spec_from_file_location("tronbyte_repository", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["tronbyte_repository"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module.TronbyteRepository
|
||||
|
||||
|
||||
def _validate_and_sanitize_app_id(app_id, fallback_source=None):
|
||||
"""Validate and sanitize app_id to a safe slug."""
|
||||
if not app_id and fallback_source:
|
||||
app_id = fallback_source
|
||||
if not app_id:
|
||||
return None, "app_id is required"
|
||||
if '..' in app_id or '/' in app_id or '\\' in app_id:
|
||||
return None, "app_id contains invalid characters"
|
||||
|
||||
sanitized = re.sub(r'[^a-z0-9_]', '_', app_id.lower()).strip('_')
|
||||
if not sanitized:
|
||||
sanitized = f"app_{hashlib.sha256(app_id.encode()).hexdigest()[:12]}"
|
||||
if sanitized[0].isdigit():
|
||||
sanitized = f"app_{sanitized}"
|
||||
return sanitized, None
|
||||
|
||||
|
||||
def _validate_timing_value(value, field_name, min_val=1, max_val=86400):
|
||||
"""Validate and coerce timing values."""
|
||||
if value is None:
|
||||
return None, None
|
||||
try:
|
||||
int_value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
return None, f"{field_name} must be an integer"
|
||||
if int_value < min_val:
|
||||
return None, f"{field_name} must be at least {min_val}"
|
||||
if int_value > max_val:
|
||||
return None, f"{field_name} must be at most {max_val}"
|
||||
return int_value, None
|
||||
|
||||
|
||||
def _get_starlark_plugin():
|
||||
"""Get the starlark-apps plugin instance, or None."""
|
||||
if not api_v3.plugin_manager:
|
||||
return None
|
||||
return api_v3.plugin_manager.get_plugin('starlark-apps')
|
||||
|
||||
|
||||
@api_v3.route('/starlark/status', methods=['GET'])
|
||||
def get_starlark_status():
|
||||
"""Get Starlark plugin status and Pixlet availability."""
|
||||
try:
|
||||
if not api_v3.plugin_manager:
|
||||
return jsonify({'status': 'error', 'message': 'Plugin manager not initialized', 'pixlet_available': False}), 500
|
||||
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if starlark_plugin:
|
||||
info = starlark_plugin.get_info()
|
||||
magnify_info = starlark_plugin.get_magnify_recommendation()
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'pixlet_available': info.get('pixlet_available', False),
|
||||
'pixlet_version': info.get('pixlet_version'),
|
||||
'installed_apps': info.get('installed_apps', 0),
|
||||
'enabled_apps': info.get('enabled_apps', 0),
|
||||
'current_app': info.get('current_app'),
|
||||
'plugin_enabled': starlark_plugin.enabled,
|
||||
'display_info': magnify_info
|
||||
})
|
||||
|
||||
# Plugin not loaded - check Pixlet availability directly
|
||||
import shutil
|
||||
import platform
|
||||
|
||||
system = platform.system().lower()
|
||||
machine = platform.machine().lower()
|
||||
bin_dir = PROJECT_ROOT / 'bin' / 'pixlet'
|
||||
|
||||
pixlet_binary = None
|
||||
if system == "linux":
|
||||
if "aarch64" in machine or "arm64" in machine:
|
||||
pixlet_binary = bin_dir / "pixlet-linux-arm64"
|
||||
elif "x86_64" in machine or "amd64" in machine:
|
||||
pixlet_binary = bin_dir / "pixlet-linux-amd64"
|
||||
elif system == "darwin":
|
||||
pixlet_binary = bin_dir / ("pixlet-darwin-arm64" if "arm64" in machine else "pixlet-darwin-amd64")
|
||||
|
||||
pixlet_available = (pixlet_binary and pixlet_binary.exists()) or shutil.which('pixlet') is not None
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'pixlet_available': pixlet_available,
|
||||
'pixlet_version': None,
|
||||
'installed_apps': 0,
|
||||
'enabled_apps': 0,
|
||||
'plugin_enabled': False,
|
||||
'plugin_loaded': False,
|
||||
'display_info': {}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting starlark status: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/apps', methods=['GET'])
|
||||
def get_starlark_apps():
|
||||
"""List all installed Starlark apps."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'success', 'apps': [], 'count': 0, 'message': 'Plugin not loaded'})
|
||||
|
||||
apps_list = []
|
||||
for app_id, app_instance in starlark_plugin.apps.items():
|
||||
apps_list.append({
|
||||
'id': app_id,
|
||||
'name': app_instance.manifest.get('name', app_id),
|
||||
'enabled': app_instance.is_enabled(),
|
||||
'has_frames': app_instance.frames is not None,
|
||||
'render_interval': app_instance.get_render_interval(),
|
||||
'display_duration': app_instance.get_display_duration(),
|
||||
'config': app_instance.config,
|
||||
'has_schema': app_instance.schema is not None,
|
||||
'last_render_time': app_instance.last_render_time
|
||||
})
|
||||
|
||||
return jsonify({'status': 'success', 'apps': apps_list, 'count': len(apps_list)})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting starlark apps: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/apps/<app_id>', methods=['GET'])
|
||||
def get_starlark_app(app_id):
|
||||
"""Get details for a specific Starlark app."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'error', 'message': 'Starlark Apps plugin not loaded'}), 404
|
||||
|
||||
app = starlark_plugin.apps.get(app_id)
|
||||
if not app:
|
||||
return jsonify({'status': 'error', 'message': f'App not found: {app_id}'}), 404
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'app': {
|
||||
'id': app_id,
|
||||
'name': app.manifest.get('name', app_id),
|
||||
'enabled': app.is_enabled(),
|
||||
'config': app.config,
|
||||
'schema': app.schema,
|
||||
'render_interval': app.get_render_interval(),
|
||||
'display_duration': app.get_display_duration(),
|
||||
'has_frames': app.frames is not None,
|
||||
'frame_count': len(app.frames) if app.frames else 0,
|
||||
'last_render_time': app.last_render_time,
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting starlark app {app_id}: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/upload', methods=['POST'])
|
||||
def upload_starlark_app():
|
||||
"""Upload and install a new Starlark app."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'error', 'message': 'Starlark Apps plugin not loaded'}), 404
|
||||
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'status': 'error', 'message': 'No file uploaded'}), 400
|
||||
|
||||
file = request.files['file']
|
||||
if not file.filename or not file.filename.endswith('.star'):
|
||||
return jsonify({'status': 'error', 'message': 'File must have .star extension'}), 400
|
||||
|
||||
app_name = request.form.get('name')
|
||||
app_id_input = request.form.get('app_id')
|
||||
filename_base = file.filename.replace('.star', '') if file.filename else None
|
||||
app_id, app_id_error = _validate_and_sanitize_app_id(app_id_input, fallback_source=filename_base)
|
||||
if app_id_error:
|
||||
return jsonify({'status': 'error', 'message': f'Invalid app_id: {app_id_error}'}), 400
|
||||
|
||||
render_interval_input = request.form.get('render_interval')
|
||||
render_interval = 300
|
||||
if render_interval_input is not None:
|
||||
render_interval, err = _validate_timing_value(render_interval_input, 'render_interval')
|
||||
if err:
|
||||
return jsonify({'status': 'error', 'message': err}), 400
|
||||
render_interval = render_interval or 300
|
||||
|
||||
display_duration_input = request.form.get('display_duration')
|
||||
display_duration = 15
|
||||
if display_duration_input is not None:
|
||||
display_duration, err = _validate_timing_value(display_duration_input, 'display_duration')
|
||||
if err:
|
||||
return jsonify({'status': 'error', 'message': err}), 400
|
||||
display_duration = display_duration or 15
|
||||
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.star') as tmp:
|
||||
file.save(tmp.name)
|
||||
temp_path = tmp.name
|
||||
|
||||
try:
|
||||
metadata = {'name': app_name or app_id, 'render_interval': render_interval, 'display_duration': display_duration}
|
||||
success = starlark_plugin.install_app(app_id, temp_path, metadata)
|
||||
if success:
|
||||
return jsonify({'status': 'success', 'message': f'App installed: {app_id}', 'app_id': app_id})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Failed to install app'}), 500
|
||||
finally:
|
||||
try:
|
||||
os.unlink(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error uploading starlark app: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/apps/<app_id>', methods=['DELETE'])
|
||||
def uninstall_starlark_app(app_id):
|
||||
"""Uninstall a Starlark app."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'error', 'message': 'Starlark Apps plugin not loaded'}), 404
|
||||
|
||||
success = starlark_plugin.uninstall_app(app_id)
|
||||
if success:
|
||||
return jsonify({'status': 'success', 'message': f'App uninstalled: {app_id}'})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Failed to uninstall app'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error uninstalling starlark app {app_id}: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/apps/<app_id>/config', methods=['GET'])
|
||||
def get_starlark_app_config(app_id):
|
||||
"""Get configuration for a Starlark app."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'error', 'message': 'Plugin not loaded'}), 404
|
||||
|
||||
app = starlark_plugin.apps.get(app_id)
|
||||
if not app:
|
||||
return jsonify({'status': 'error', 'message': f'App not found: {app_id}'}), 404
|
||||
|
||||
return jsonify({'status': 'success', 'config': app.config, 'schema': app.schema})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting config for {app_id}: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/apps/<app_id>/config', methods=['PUT'])
|
||||
def update_starlark_app_config(app_id):
|
||||
"""Update configuration for a Starlark app."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'error', 'message': 'Plugin not loaded'}), 404
|
||||
|
||||
app = starlark_plugin.apps.get(app_id)
|
||||
if not app:
|
||||
return jsonify({'status': 'error', 'message': f'App not found: {app_id}'}), 404
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'status': 'error', 'message': 'No configuration provided'}), 400
|
||||
|
||||
if 'render_interval' in data:
|
||||
val, err = _validate_timing_value(data['render_interval'], 'render_interval')
|
||||
if err:
|
||||
return jsonify({'status': 'error', 'message': err}), 400
|
||||
data['render_interval'] = val
|
||||
|
||||
if 'display_duration' in data:
|
||||
val, err = _validate_timing_value(data['display_duration'], 'display_duration')
|
||||
if err:
|
||||
return jsonify({'status': 'error', 'message': err}), 400
|
||||
data['display_duration'] = val
|
||||
|
||||
app.config.update(data)
|
||||
if app.save_config():
|
||||
starlark_plugin._render_app(app, force=True)
|
||||
return jsonify({'status': 'success', 'message': 'Configuration updated', 'config': app.config})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Failed to save configuration'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating config for {app_id}: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/apps/<app_id>/toggle', methods=['POST'])
|
||||
def toggle_starlark_app(app_id):
|
||||
"""Enable or disable a Starlark app."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'error', 'message': 'Plugin not loaded'}), 404
|
||||
|
||||
app = starlark_plugin.apps.get(app_id)
|
||||
if not app:
|
||||
return jsonify({'status': 'error', 'message': f'App not found: {app_id}'}), 404
|
||||
|
||||
data = request.get_json() or {}
|
||||
enabled = data.get('enabled')
|
||||
if enabled is None:
|
||||
enabled = not app.is_enabled()
|
||||
|
||||
app.manifest['enabled'] = enabled
|
||||
|
||||
with open(starlark_plugin.manifest_file, 'r') as f:
|
||||
manifest = json.load(f)
|
||||
manifest['apps'][app_id]['enabled'] = enabled
|
||||
starlark_plugin._save_manifest(manifest)
|
||||
|
||||
return jsonify({'status': 'success', 'message': f"App {'enabled' if enabled else 'disabled'}", 'enabled': enabled})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error toggling app {app_id}: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/apps/<app_id>/render', methods=['POST'])
|
||||
def render_starlark_app(app_id):
|
||||
"""Force render a Starlark app."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'error', 'message': 'Plugin not loaded'}), 404
|
||||
|
||||
app = starlark_plugin.apps.get(app_id)
|
||||
if not app:
|
||||
return jsonify({'status': 'error', 'message': f'App not found: {app_id}'}), 404
|
||||
|
||||
success = starlark_plugin._render_app(app, force=True)
|
||||
if success:
|
||||
return jsonify({'status': 'success', 'message': 'App rendered', 'frame_count': len(app.frames) if app.frames else 0})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Failed to render app'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error rendering app {app_id}: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/repository/browse', methods=['GET'])
|
||||
def browse_tronbyte_repository():
|
||||
"""Browse apps in the Tronbyte repository."""
|
||||
try:
|
||||
TronbyteRepository = _get_tronbyte_repository_class()
|
||||
|
||||
config = api_v3.config_manager.load_config() if api_v3.config_manager else {}
|
||||
github_token = config.get('github_token')
|
||||
repo = TronbyteRepository(github_token=github_token)
|
||||
|
||||
search_query = request.args.get('search', '')
|
||||
category = request.args.get('category', 'all')
|
||||
limit = max(1, min(request.args.get('limit', 50, type=int), 200))
|
||||
|
||||
apps = repo.list_apps_with_metadata(max_apps=limit)
|
||||
if search_query:
|
||||
apps = repo.search_apps(search_query, apps)
|
||||
if category and category != 'all':
|
||||
apps = repo.filter_by_category(category, apps)
|
||||
|
||||
rate_limit = repo.get_rate_limit_info()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'apps': apps,
|
||||
'count': len(apps),
|
||||
'rate_limit': rate_limit,
|
||||
'filters': {'search': search_query, 'category': category}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error browsing repository: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/repository/install', methods=['POST'])
|
||||
def install_from_tronbyte_repository():
|
||||
"""Install an app from the Tronbyte repository."""
|
||||
try:
|
||||
starlark_plugin = _get_starlark_plugin()
|
||||
if not starlark_plugin:
|
||||
return jsonify({'status': 'error', 'message': 'Plugin not loaded'}), 404
|
||||
|
||||
data = request.get_json()
|
||||
if not data or 'app_id' not in data:
|
||||
return jsonify({'status': 'error', 'message': 'app_id is required'}), 400
|
||||
|
||||
app_id, app_id_error = _validate_and_sanitize_app_id(data['app_id'])
|
||||
if app_id_error:
|
||||
return jsonify({'status': 'error', 'message': f'Invalid app_id: {app_id_error}'}), 400
|
||||
|
||||
TronbyteRepository = _get_tronbyte_repository_class()
|
||||
import tempfile
|
||||
|
||||
config = api_v3.config_manager.load_config() if api_v3.config_manager else {}
|
||||
github_token = config.get('github_token')
|
||||
repo = TronbyteRepository(github_token=github_token)
|
||||
|
||||
success, metadata, error = repo.get_app_metadata(data['app_id'])
|
||||
if not success:
|
||||
return jsonify({'status': 'error', 'message': f'Failed to fetch app metadata: {error}'}), 404
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.star') as tmp:
|
||||
temp_path = tmp.name
|
||||
|
||||
try:
|
||||
success, error = repo.download_star_file(data['app_id'], Path(temp_path))
|
||||
if not success:
|
||||
return jsonify({'status': 'error', 'message': f'Failed to download app: {error}'}), 500
|
||||
|
||||
render_interval = data.get('render_interval', 300)
|
||||
ri, err = _validate_timing_value(render_interval, 'render_interval')
|
||||
if err:
|
||||
return jsonify({'status': 'error', 'message': err}), 400
|
||||
render_interval = ri or 300
|
||||
|
||||
display_duration = data.get('display_duration', 15)
|
||||
dd, err = _validate_timing_value(display_duration, 'display_duration')
|
||||
if err:
|
||||
return jsonify({'status': 'error', 'message': err}), 400
|
||||
display_duration = dd or 15
|
||||
|
||||
install_metadata = {
|
||||
'name': metadata.get('name', app_id) if metadata else app_id,
|
||||
'render_interval': render_interval,
|
||||
'display_duration': display_duration
|
||||
}
|
||||
|
||||
success = starlark_plugin.install_app(data['app_id'], temp_path, install_metadata)
|
||||
if success:
|
||||
return jsonify({'status': 'success', 'message': f'App installed: {metadata.get("name", app_id) if metadata else app_id}', 'app_id': app_id})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Failed to install app'}), 500
|
||||
finally:
|
||||
try:
|
||||
os.unlink(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing from repository: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/repository/categories', methods=['GET'])
|
||||
def get_tronbyte_categories():
|
||||
"""Get list of available app categories."""
|
||||
try:
|
||||
TronbyteRepository = _get_tronbyte_repository_class()
|
||||
config = api_v3.config_manager.load_config() if api_v3.config_manager else {}
|
||||
repo = TronbyteRepository(github_token=config.get('github_token'))
|
||||
|
||||
apps = repo.list_apps_with_metadata(max_apps=100)
|
||||
categories = sorted({app.get('category', '') for app in apps if app.get('category')})
|
||||
|
||||
return jsonify({'status': 'success', 'categories': categories})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching categories: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/starlark/install-pixlet', methods=['POST'])
|
||||
def install_pixlet():
|
||||
"""Download and install Pixlet binary."""
|
||||
try:
|
||||
script_path = PROJECT_ROOT / 'scripts' / 'download_pixlet.sh'
|
||||
if not script_path.exists():
|
||||
return jsonify({'status': 'error', 'message': 'Installation script not found'}), 404
|
||||
|
||||
os.chmod(script_path, 0o755)
|
||||
|
||||
result = subprocess.run(
|
||||
[str(script_path)],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.info("Pixlet downloaded successfully")
|
||||
return jsonify({'status': 'success', 'message': 'Pixlet installed successfully!', 'output': result.stdout})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': f'Failed to download Pixlet: {result.stderr}'}), 500
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return jsonify({'status': 'error', 'message': 'Download timed out'}), 500
|
||||
except Exception as e:
|
||||
logger.error(f"Error installing Pixlet: {e}")
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
@@ -322,7 +322,11 @@ def _load_plugin_config_partial(plugin_id):
|
||||
try:
|
||||
if not pages_v3.plugin_manager:
|
||||
return '<div class="text-red-500 p-4">Plugin manager not available</div>', 500
|
||||
|
||||
|
||||
# Handle starlark app config (starlark:<app_id>)
|
||||
if plugin_id.startswith('starlark:'):
|
||||
return _load_starlark_config_partial(plugin_id[len('starlark:'):])
|
||||
|
||||
# Try to get plugin info first
|
||||
plugin_info = pages_v3.plugin_manager.get_plugin_info(plugin_id)
|
||||
|
||||
@@ -429,3 +433,34 @@ def _load_plugin_config_partial(plugin_id):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return f'<div class="text-red-500 p-4">Error loading plugin config: {str(e)}</div>', 500
|
||||
|
||||
|
||||
def _load_starlark_config_partial(app_id):
|
||||
"""Load configuration partial for a Starlark app."""
|
||||
try:
|
||||
starlark_plugin = pages_v3.plugin_manager.get_plugin('starlark-apps')
|
||||
if not starlark_plugin:
|
||||
return '<div class="text-yellow-600 p-4"><i class="fas fa-exclamation-triangle mr-2"></i>Starlark Apps plugin not loaded</div>', 404
|
||||
|
||||
app = starlark_plugin.apps.get(app_id)
|
||||
if not app:
|
||||
return f'<div class="text-red-500 p-4">Starlark app not found: {app_id}</div>', 404
|
||||
|
||||
return render_template(
|
||||
'v3/partials/starlark_config.html',
|
||||
app_id=app_id,
|
||||
app_name=app.manifest.get('name', app_id),
|
||||
app_enabled=app.is_enabled(),
|
||||
render_interval=app.get_render_interval(),
|
||||
display_duration=app.get_display_duration(),
|
||||
config=app.config,
|
||||
schema=app.schema,
|
||||
has_frames=app.frames is not None,
|
||||
frame_count=len(app.frames) if app.frames else 0,
|
||||
last_render_time=app.last_render_time,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return f'<div class="text-red-500 p-4">Error loading starlark config: {str(e)}</div>', 500
|
||||
|
||||
@@ -1399,6 +1399,7 @@ function renderInstalledPlugins(plugins) {
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center flex-wrap gap-2 mb-2">
|
||||
<h4 class="font-semibold text-gray-900 text-base">${escapeHtml(plugin.name || plugin.id)}</h4>
|
||||
${plugin.is_starlark_app ? '<span class="badge badge-warning"><i class="fas fa-star mr-1"></i>Starlark</span>' : ''}
|
||||
${plugin.verified ? '<span class="badge badge-success"><i class="fas fa-check-circle mr-1"></i>Verified</span>' : ''}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-1.5 mb-3">
|
||||
@@ -1610,18 +1611,37 @@ function handlePluginAction(event) {
|
||||
});
|
||||
break;
|
||||
case 'uninstall':
|
||||
waitForFunction('uninstallPlugin', 10, 50)
|
||||
.then(uninstallFunc => {
|
||||
uninstallFunc(pluginId);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[EVENT DELEGATION]', error.message);
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Uninstall function not loaded. Please refresh the page.', 'error');
|
||||
} else {
|
||||
alert('Uninstall function not loaded. Please refresh the page.');
|
||||
}
|
||||
});
|
||||
if (pluginId.startsWith('starlark:')) {
|
||||
// Starlark app uninstall uses dedicated endpoint
|
||||
const starlarkAppId = pluginId.slice('starlark:'.length);
|
||||
if (!confirm(`Uninstall Starlark app "${starlarkAppId}"?`)) break;
|
||||
fetch(`/api/v3/starlark/apps/${encodeURIComponent(starlarkAppId)}`, {method: 'DELETE'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
if (typeof showNotification === 'function') showNotification('Starlark app uninstalled', 'success');
|
||||
else alert('Starlark app uninstalled');
|
||||
if (typeof loadInstalledPlugins === 'function') loadInstalledPlugins();
|
||||
else if (typeof window.loadInstalledPlugins === 'function') window.loadInstalledPlugins();
|
||||
} else {
|
||||
alert('Uninstall failed: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Uninstall failed: ' + err.message));
|
||||
} else {
|
||||
waitForFunction('uninstallPlugin', 10, 50)
|
||||
.then(uninstallFunc => {
|
||||
uninstallFunc(pluginId);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[EVENT DELEGATION]', error.message);
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Uninstall function not loaded. Please refresh the page.', 'error');
|
||||
} else {
|
||||
alert('Uninstall function not loaded. Please refresh the page.');
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -7297,3 +7317,246 @@ setTimeout(function() {
|
||||
}, 500);
|
||||
}, 200);
|
||||
|
||||
// ─── Starlark Apps Integration ──────────────────────────────────────────────
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
let starlarkSectionVisible = false;
|
||||
let starlarkAppsCache = null;
|
||||
|
||||
function initStarlarkSection() {
|
||||
const toggleBtn = document.getElementById('toggle-starlark-section');
|
||||
if (toggleBtn && !toggleBtn._starlarkInit) {
|
||||
toggleBtn._starlarkInit = true;
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
starlarkSectionVisible = !starlarkSectionVisible;
|
||||
const content = document.getElementById('starlark-section-content');
|
||||
const icon = document.getElementById('starlark-section-icon');
|
||||
if (content) content.classList.toggle('hidden', !starlarkSectionVisible);
|
||||
if (icon) {
|
||||
icon.classList.toggle('fa-chevron-down', !starlarkSectionVisible);
|
||||
icon.classList.toggle('fa-chevron-up', starlarkSectionVisible);
|
||||
}
|
||||
this.querySelector('span').textContent = starlarkSectionVisible ? 'Hide' : 'Show';
|
||||
if (starlarkSectionVisible) {
|
||||
loadStarlarkStatus();
|
||||
loadStarlarkCategories();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const browseBtn = document.getElementById('starlark-browse-btn');
|
||||
if (browseBtn && !browseBtn._starlarkInit) {
|
||||
browseBtn._starlarkInit = true;
|
||||
browseBtn.addEventListener('click', browseStarlarkApps);
|
||||
}
|
||||
|
||||
const uploadBtn = document.getElementById('starlark-upload-btn');
|
||||
if (uploadBtn && !uploadBtn._starlarkInit) {
|
||||
uploadBtn._starlarkInit = true;
|
||||
uploadBtn.addEventListener('click', function() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.star';
|
||||
input.onchange = function(e) {
|
||||
if (e.target.files.length > 0) uploadStarlarkFile(e.target.files[0]);
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadStarlarkStatus() {
|
||||
fetch('/api/v3/starlark/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const banner = document.getElementById('starlark-pixlet-status');
|
||||
if (!banner) return;
|
||||
if (data.pixlet_available) {
|
||||
banner.innerHTML = `<div class="bg-green-50 border border-green-200 rounded-lg p-3 text-sm text-green-800">
|
||||
<i class="fas fa-check-circle mr-2"></i>Pixlet available${data.pixlet_version ? ' (' + escapeHtml(data.pixlet_version) + ')' : ''} — ${data.installed_apps || 0} app(s) installed
|
||||
</div>`;
|
||||
} else {
|
||||
banner.innerHTML = `<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-800">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>Pixlet not installed.
|
||||
<button onclick="window.installPixlet()" class="ml-2 px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-semibold">Install Pixlet</button>
|
||||
</div>`;
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Starlark status error:', err));
|
||||
}
|
||||
|
||||
function loadStarlarkCategories() {
|
||||
fetch('/api/v3/starlark/repository/categories')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
const select = document.getElementById('starlark-category');
|
||||
if (!select) return;
|
||||
select.innerHTML = '<option value="">All Categories</option>';
|
||||
(data.categories || []).forEach(cat => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = cat;
|
||||
opt.textContent = cat;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
})
|
||||
.catch(err => console.error('Starlark categories error:', err));
|
||||
}
|
||||
|
||||
function browseStarlarkApps() {
|
||||
const search = (document.getElementById('starlark-search') || {}).value || '';
|
||||
const category = (document.getElementById('starlark-category') || {}).value || '';
|
||||
const grid = document.getElementById('starlark-apps-grid');
|
||||
const countEl = document.getElementById('starlark-apps-count');
|
||||
|
||||
if (grid) grid.innerHTML = '<div class="col-span-full text-center py-8 text-gray-500"><i class="fas fa-spinner fa-spin mr-2"></i>Loading Tronbyte apps...</div>';
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (search) params.set('search', search);
|
||||
if (category) params.set('category', category);
|
||||
params.set('limit', '50');
|
||||
|
||||
fetch('/api/v3/starlark/repository/browse?' + params.toString())
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status !== 'success') {
|
||||
if (grid) grid.innerHTML = `<div class="col-span-full text-center py-8 text-red-500"><i class="fas fa-exclamation-circle mr-2"></i>${escapeHtml(data.message || 'Failed to load')}</div>`;
|
||||
return;
|
||||
}
|
||||
starlarkAppsCache = data.apps;
|
||||
if (countEl) countEl.textContent = `${data.count} apps`;
|
||||
renderStarlarkApps(data.apps, grid);
|
||||
|
||||
if (data.rate_limit) {
|
||||
const rl = data.rate_limit;
|
||||
console.log(`[Starlark] GitHub rate limit: ${rl.remaining}/${rl.limit} remaining`);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Starlark browse error:', err);
|
||||
if (grid) grid.innerHTML = '<div class="col-span-full text-center py-8 text-red-500"><i class="fas fa-exclamation-circle mr-2"></i>Error loading apps</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderStarlarkApps(apps, grid) {
|
||||
if (!grid) return;
|
||||
if (!apps || apps.length === 0) {
|
||||
grid.innerHTML = '<div class="col-span-full empty-state"><div class="empty-state-icon"><i class="fas fa-star"></i></div><p>No Starlark apps found</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = apps.map(app => `
|
||||
<div class="plugin-card">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center flex-wrap gap-2 mb-2">
|
||||
<h4 class="font-semibold text-gray-900 text-base">${escapeHtml(app.name || app.id)}</h4>
|
||||
<span class="badge badge-warning"><i class="fas fa-star mr-1"></i>Starlark</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-1.5 mb-3">
|
||||
${app.author ? `<p class="flex items-center"><i class="fas fa-user mr-2 text-gray-400 w-4"></i>${escapeHtml(app.author)}</p>` : ''}
|
||||
${app.category ? `<p class="flex items-center"><i class="fas fa-folder mr-2 text-gray-400 w-4"></i>${escapeHtml(app.category)}</p>` : ''}
|
||||
</div>
|
||||
<p class="text-sm text-gray-700 leading-relaxed">${escapeHtml(app.summary || app.desc || 'No description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:0.5rem; margin-top:1rem; padding-top:1rem; border-top:1px solid #e5e7eb;">
|
||||
<button onclick="window.installStarlarkApp('${escapeHtml(app.id)}')" class="btn bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-semibold" style="flex:1; display:flex; justify-content:center;">
|
||||
<i class="fas fa-download mr-2"></i>Install
|
||||
</button>
|
||||
<button onclick="window.open('https://github.com/tronbyt/apps/tree/main/apps/${encodeURIComponent(app.id)}', '_blank')" class="btn bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-semibold" style="display:flex; justify-content:center;">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
window.installStarlarkApp = function(appId) {
|
||||
if (!confirm(`Install Starlark app "${appId}" from Tronbyte repository?`)) return;
|
||||
|
||||
fetch('/api/v3/starlark/repository/install', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({app_id: appId})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert(`Installed: ${data.message || appId}`);
|
||||
// Refresh installed plugins to show the new starlark app
|
||||
if (typeof loadInstalledPlugins === 'function') loadInstalledPlugins();
|
||||
else if (typeof window.loadInstalledPlugins === 'function') window.loadInstalledPlugins();
|
||||
} else {
|
||||
alert(`Install failed: ${data.message || 'Unknown error'}`);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Install error:', err);
|
||||
alert('Install failed: ' + err.message);
|
||||
});
|
||||
};
|
||||
|
||||
window.installPixlet = function() {
|
||||
if (!confirm('Download and install Pixlet binary? This may take a few minutes.')) return;
|
||||
|
||||
fetch('/api/v3/starlark/install-pixlet', {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert(data.message || 'Pixlet installed!');
|
||||
loadStarlarkStatus();
|
||||
} else {
|
||||
alert('Pixlet install failed: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Pixlet install failed: ' + err.message));
|
||||
};
|
||||
|
||||
function uploadStarlarkFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const appId = file.name.replace('.star', '');
|
||||
formData.append('app_id', appId);
|
||||
formData.append('name', appId.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()));
|
||||
|
||||
fetch('/api/v3/starlark/upload', {method: 'POST', body: formData})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert(`Uploaded: ${data.app_id}`);
|
||||
if (typeof loadInstalledPlugins === 'function') loadInstalledPlugins();
|
||||
else if (typeof window.loadInstalledPlugins === 'function') window.loadInstalledPlugins();
|
||||
} else {
|
||||
alert('Upload failed: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Upload failed: ' + err.message));
|
||||
}
|
||||
|
||||
// Initialize when plugins tab loads
|
||||
const origInit = window.initializePlugins;
|
||||
window.initializePlugins = function() {
|
||||
if (origInit) origInit();
|
||||
initStarlarkSection();
|
||||
};
|
||||
|
||||
// Also try to init on DOMContentLoaded and on HTMX load
|
||||
document.addEventListener('DOMContentLoaded', initStarlarkSection);
|
||||
document.addEventListener('htmx:afterSwap', function(e) {
|
||||
if (e.detail && e.detail.target && e.detail.target.id === 'plugins-content') {
|
||||
initStarlarkSection();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@@ -179,6 +179,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Starlark Apps Section (Tronbyte Community Apps) -->
|
||||
<div id="starlark-apps-section" class="border-t border-gray-200 pt-8 mt-8">
|
||||
<div class="flex items-center justify-between mb-5 pb-3 border-b border-gray-200">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-bold text-gray-900"><i class="fas fa-star text-yellow-500 mr-2"></i>Starlark Apps</h3>
|
||||
<span id="starlark-apps-count" class="text-sm text-gray-500 font-medium"></span>
|
||||
</div>
|
||||
<button id="toggle-starlark-section" class="text-sm text-blue-600 hover:text-blue-800 flex items-center font-medium transition-colors">
|
||||
<i class="fas fa-chevron-down mr-1" id="starlark-section-icon"></i>
|
||||
<span>Show</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="starlark-section-content" class="hidden">
|
||||
<p class="text-sm text-gray-600 mb-4">Browse and install Starlark apps from the <a href="https://github.com/tronbyt/apps" target="_blank" class="text-blue-600 hover:text-blue-800 underline">Tronbyte community repository</a>. Requires <strong>Pixlet</strong> binary.</p>
|
||||
|
||||
<!-- Pixlet Status Banner -->
|
||||
<div id="starlark-pixlet-status" class="mb-4"></div>
|
||||
|
||||
<!-- Search/Filter -->
|
||||
<div class="flex gap-3 mb-4">
|
||||
<input type="text" id="starlark-search" placeholder="Search Starlark apps..." class="form-control text-sm flex-[3] min-w-0 px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm">
|
||||
<select id="starlark-category" class="form-control text-sm flex-1 px-3 py-2.5 border border-gray-300 rounded-lg shadow-sm">
|
||||
<option value="">All Categories</option>
|
||||
</select>
|
||||
<button id="starlark-browse-btn" class="btn bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg whitespace-nowrap font-semibold shadow-sm">
|
||||
<i class="fas fa-search mr-2"></i>Browse
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Upload .star file -->
|
||||
<div class="flex gap-3 mb-4">
|
||||
<button id="starlark-upload-btn" class="btn bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm">
|
||||
<i class="fas fa-upload mr-2"></i>Upload .star File
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Starlark Apps Grid -->
|
||||
<div id="starlark-apps-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Install from GitHub URL Section (Separate section, always visible) -->
|
||||
<div class="border-t border-gray-200 pt-8 mt-8">
|
||||
<div class="flex items-center justify-between mb-5 pb-3 border-b border-gray-200">
|
||||
|
||||
160
web_interface/templates/v3/partials/starlark_config.html
Normal file
160
web_interface/templates/v3/partials/starlark_config.html
Normal file
@@ -0,0 +1,160 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between pb-4 border-b border-gray-200">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-gray-900">
|
||||
<i class="fas fa-star text-yellow-500 mr-2"></i>{{ app_name }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">Starlark App — ID: {{ app_id }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="badge badge-warning"><i class="fas fa-star mr-1"></i>Starlark</span>
|
||||
{% if app_enabled %}
|
||||
<span class="badge badge-success">Enabled</span>
|
||||
{% else %}
|
||||
<span class="badge badge-error">Disabled</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3">Status</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-500">Frames:</span>
|
||||
<span class="font-medium ml-1">{{ frame_count if has_frames else 'Not rendered' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Render Interval:</span>
|
||||
<span class="font-medium ml-1">{{ render_interval }}s</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Display Duration:</span>
|
||||
<span class="font-medium ml-1">{{ display_duration }}s</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Last Render:</span>
|
||||
<span class="font-medium ml-1" id="starlark-last-render">{{ last_render_time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3">
|
||||
<button onclick="forceRenderStarlarkApp('{{ app_id }}')"
|
||||
class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-semibold">
|
||||
<i class="fas fa-sync mr-2"></i>Force Render
|
||||
</button>
|
||||
<button onclick="toggleStarlarkApp('{{ app_id }}', {{ 'false' if app_enabled else 'true' }})"
|
||||
class="btn {{ 'bg-red-600 hover:bg-red-700' if app_enabled else 'bg-green-600 hover:bg-green-700' }} text-white px-4 py-2 rounded-md text-sm font-semibold">
|
||||
<i class="fas {{ 'fa-toggle-off' if app_enabled else 'fa-toggle-on' }} mr-2"></i>
|
||||
{{ 'Disable' if app_enabled else 'Enable' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- App-specific Config (if schema exists) -->
|
||||
{% if schema %}
|
||||
<div class="bg-white rounded-lg p-4 border border-gray-200">
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3">App Configuration</h4>
|
||||
<div id="starlark-config-form" class="space-y-4">
|
||||
{% for key, value in config.items() %}
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ key }}</label>
|
||||
<input type="text" class="form-control w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
name="{{ key }}" value="{{ value }}"
|
||||
data-starlark-config="{{ key }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button onclick="saveStarlarkConfig('{{ app_id }}')"
|
||||
class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-semibold">
|
||||
<i class="fas fa-save mr-2"></i>Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% elif config %}
|
||||
<div class="bg-white rounded-lg p-4 border border-gray-200">
|
||||
<h4 class="text-sm font-semibold text-gray-700 mb-3">App Configuration</h4>
|
||||
<div id="starlark-config-form" class="space-y-4">
|
||||
{% for key, value in config.items() %}
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ key }}</label>
|
||||
<input type="text" class="form-control w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
name="{{ key }}" value="{{ value }}"
|
||||
data-starlark-config="{{ key }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button onclick="saveStarlarkConfig('{{ app_id }}')"
|
||||
class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-semibold">
|
||||
<i class="fas fa-save mr-2"></i>Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-gray-50 rounded-lg p-4 border border-gray-200 text-center text-gray-500 text-sm">
|
||||
<i class="fas fa-info-circle mr-1"></i>This app has no configurable settings.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function forceRenderStarlarkApp(appId) {
|
||||
fetch('/api/v3/starlark/apps/' + encodeURIComponent(appId) + '/render', {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert('Rendered successfully! ' + (data.frame_count || 0) + ' frame(s)');
|
||||
} else {
|
||||
alert('Render failed: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Render failed: ' + err.message));
|
||||
}
|
||||
|
||||
function toggleStarlarkApp(appId, enabled) {
|
||||
fetch('/api/v3/starlark/apps/' + encodeURIComponent(appId) + '/toggle', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({enabled: enabled})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Reload the config partial to reflect new state
|
||||
if (typeof loadInstalledPlugins === 'function') loadInstalledPlugins();
|
||||
else if (typeof window.loadInstalledPlugins === 'function') window.loadInstalledPlugins();
|
||||
// Reload this partial
|
||||
const container = document.getElementById('plugin-config-starlark:' + appId);
|
||||
if (container && window.htmx) {
|
||||
htmx.ajax('GET', '/v3/partials/plugin-config/starlark:' + encodeURIComponent(appId), {target: container, swap: 'innerHTML'});
|
||||
}
|
||||
} else {
|
||||
alert('Toggle failed: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Toggle failed: ' + err.message));
|
||||
}
|
||||
|
||||
function saveStarlarkConfig(appId) {
|
||||
const inputs = document.querySelectorAll('[data-starlark-config]');
|
||||
const config = {};
|
||||
inputs.forEach(input => {
|
||||
config[input.getAttribute('data-starlark-config')] = input.value;
|
||||
});
|
||||
|
||||
fetch('/api/v3/starlark/apps/' + encodeURIComponent(appId) + '/config', {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(config)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert('Configuration saved!');
|
||||
} else {
|
||||
alert('Save failed: ' + (data.message || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Save failed: ' + err.message));
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user