Skip to content

Web Widget Overlay

This feature allows you to display web content—such as websites, local HTML files, or entire web projects contained within a ZIP file—as an overlay on your screen. You can configure it to be a full-screen interactive browser, a floating window, or a non-interactive informational widget.

Creating a Web Widget Action

  1. Start the configuration. Give your action a custom title. This is how it will appear in your mappings.

  2. Choose an icon. Click the icon box to select a visual style for your widget from the available dialog.

  3. Enter the URL or Select a File.

    • URL: Type the web address (e.g., www.youtube.com).
    • Local Files: Use "Select HTML file" or "Select ZIP file" for local projects.

    Important

    ZIP projects must contain an index.html file at the root level to load correctly. The app automatically strips a single top-level folder if all files are inside it (e.g., my-widget/index.html works fine).

  4. Configure Display and Interaction. Adjust the following behavioral switches:

    • Receive system data: When enabled, the app injects live device telemetry (CPU, RAM, network, media info, IP addresses, etc.) directly into your widget via window.onFieldChange callbacks. Only useful if your widget has those callbacks implemented. See Building a system data widget below.
    • Fullscreen: If disabled, you can set custom window dimensions.
    • Interactive (touchable): When enabled, the overlay takes focus. When disabled, the window is "transparent" to input, allowing you to control the background app.
    • Enable cursor mode: (Only if Interactive is on) Displays a virtual mouse cursor. If off, you navigate using standard DPAD directional keys.

  5. Set Position (Gravity). Choose where the widget will appear on the screen. Available positions: Center, Top Center, Bottom Center, Center Left, Center Right, Top Left, Top Right, Bottom Left, Bottom Right.

  6. Define Size. Enter the specific Width and Height in pixels for your overlay window.

  7. Save the Action. Click "Save" to finalize. The action is now ready to be used in your mappings, menus, or macros.

Examples in Action

YouTube Web Overlay In this example, YouTube opens as a side window. Since Interactive and Cursor Mode are enabled, you can use the virtual pointer to navigate the site while keeping your background app visible.

Local JS Media Player (from ZIP) This shows a media player loaded from a local ZIP file. It is configured to be Interactive, allowing you to control the JS interface directly with your remote.

Setting Up a Ready-Made Widget from a ZIP

If you already have a downloaded widget package (like the All-in-One Overlay Pack shown in the community), here is the step-by-step process to initialize and map it:

  1. Create the Action: Give your widget a custom title (e.g., "custom web widget from zip") and choose an appropriate icon.

  2. Load the widget file: Press Select ZIP file and choose your widget package from device storage, or select a single self-contained HTML file if your widget does not require extra assets or a config.json file. Note: Live callbacks such as window.onFieldChange still work in standalone HTML files, but the Configure Widget button is only available when using a ZIP package with config.json. If your widget is a simple static HTML design, you can leave Receive system data unchecked. However, if it relies on live device metrics, make sure it is enabled.

  3. Configure Settings (Optional): The Configure Widget button will only appear if the ZIP contains an internal config.json file. This allows you to customize settings without editing code.

  4. Configure Display Options: From the four available options shown, the most relevant one is Receive system data, which should only be enabled if your widget is intended to display live device telemetry such as CPU, RAM, or network usage. See Building a system data widget below. The remaining display and interaction options, like Fullscreen, Interactive (touchable), and Enable cursor mode, can then be adjusted based on how you want the widget to behave before saving.

  5. Map to a Remote Button: Navigate to the Mappings tab in tvQuickActions. Select a button and assign your new "Web Widget Overlay" action to a specific trigger.

  6. Enjoy your Overlay: Press the mapped button on your remote. The widget will appear exactly as configured, overlaying your current app or background!


Building a system data widget

Pro-Tip: Use AI to build your own custom widgets!

The technical sections below explain how this system works under the hood. While it involves HTML/JS, you don't actually need to be a programmer! Simply copy the documentation from here until the end of the page, paste it into an AI (like Gemini, ChatGPT or Claude), and tell it: "Using this documentation as context, write the HTML, CSS, and JS for a tvQuickActions widget that displays [your idea here].". It will generate the files for you!

Credit: Special thanks to @Thorfinn66 on Telegram, who designed the integration logic based on config.json and the method to inject data directly into the widget. If you need help or want to share widgets, join the community at t.me/tvquickactions.

Overview

When Receive system data is enabled, the app automatically injects dispatcher.js into your page's <head> before any other script runs. This script exposes a global window.onFieldChange dispatcher object. Your widget registers callbacks on it; the app then calls those callbacks whenever live data changes (roughly once per second for sys_* fields). The final design, the layout, and which specific data points you choose to display are entirely up to you.


ZIP file structure

Any widget ZIP must contain at minimum an index.html and, if you want the Configure Widget dialog, a config.json. Everything else is optional.

my-overlay.zip
├── index.html          ← required; entry point loaded by the app
├── config.json         ← required for the Configure Widget dialog
├── style.css           ← optional
├── script.js           ← optional
├── scripts/
│   └── my-widget.js    ← optional; recommended pattern for multi-widget ZIPs
├── fonts/
│   └── myfont.ttf      ← optional; reference via relative path in CSS
└── assets/
    └── logo.png        ← optional

ZIP root stripping

The app automatically strips one top-level folder from the ZIP if all files live inside it. So both index.html at the zip root and my-widget/index.html inside a single folder work correctly.

Exporting your widget

Once you've configured a ZIP widget, you can export it back as a ZIP (with any configuration changes saved inside config.json) using the Export button in the Configure Widget screen. The exported file is saved to your Downloads folder.


How live data is delivered

The app injects dispatcher.js into your <head> automatically. This script exposes window.onFieldChange — a global dispatcher object. You register callback functions on it, keyed by field name. They are called automatically whenever a value changes.

Two registration patterns:

Pattern 1 — Direct assignment (single listener, for non-sys_* fields)

Use this for your own config fields. Only one callback per field ID is supported.

window.onFieldChange = window.onFieldChange || {};

onFieldChange.myColor = function(value) {
    document.getElementById('box').style.color = value;
};

Pattern 2 — .add() method (multi-listener, required for sys_* fields)

Use this for all sys_* fields. Multiple widgets can all subscribe to the same system field independently without overwriting each other.

onFieldChange.add('sys_cpu_load', function(value) {
    document.getElementById('cpu').textContent = value;
});

// Another widget can also listen to the same field:
onFieldChange.add('sys_cpu_load', function(value) {
    myChart.update(value);
});

Always use .add() for sys_* fields in multi-widget ZIPs

If two widgets both do onFieldChange.sys_cpu_load = fn, the second will overwrite the first. Use .add() to avoid this.

Callback value notes

  • Values arrive already parsed: numbers as Number, booleans as Boolean, JSON objects/arrays as objects/arrays.
  • A null value means the field is unavailable or empty.
  • sys_* callbacks fire roughly once per second. Config field callbacks fire once on load and again whenever the user changes the setting in the Configure dialog.

config.json — widget configuration

The config.json file defines the user-editable fields that appear in the Configure Widget dialog inside tvQuickActions. When the user changes a value, it is written back into config.json and immediately dispatched to all registered callbacks in the running widget.

Top-level structure

{
  "title": "My Widget",
  "filename": "my-widget",
  "description": "Optional description shown in the Configure dialog.",
  "author": "YourName",
  "version": "1.0.0",
  "blocks": [ ... ]
}
Key Required Description
title yes Display name shown in the Configure Widget dialog header
filename yes Base filename used when exporting the widget as a ZIP
description no Optional multi-line description shown at the top of the Configure dialog
author no Author name, informational only
version no Version string, informational only
blocks yes Array of section blocks, each containing a group of fields

Block structure

Fields are organized into collapsible sections called blocks. Each block has a title and an array of fields.

{
  "blocks": [
    {
      "title": "Appearance",
      "fields": [ ... ]
    },
    {
      "title": "Behavior",
      "fields": [ ... ]
    }
  ]
}

Field structure

Every field requires at minimum id, title, type, and default.

{
  "id": "myFieldId",
  "title": "Label shown in dialog",
  "type": "text",
  "default": "Hello"
}

Use title, not label

The key for the displayed label is title. Using label will not work.


Available field types

Type Description Value received in callback
text Single-line text input String
number Slider with optional min, max, step Number
toggle On/off switch. Supports optional trueValue/falseValue to emit custom strings Boolean, or String if trueValue/falseValue are set
select Dropdown list. Requires options array. Supports optional optionLabels for display names String (the selected option value)
multiselect Inline list of checkboxes. Requires options array JSONArray of selected string values
multitoggle Summary row that opens a dialog of named checkboxes. Requires options array of {id, title} objects. Dispatches on every change JSONObject mapping each option idBoolean
color Hex color picker with RGB sliders String "#rrggbb"

text example
{
  "id": "headerText",
  "title": "Header text",
  "type": "text",
  "default": "My Overlay"
}
onFieldChange.add('headerText', function(value) {
    document.getElementById('header').textContent = value || '';
});

number example

The number type renders as a slider. min, max, and step are optional (defaults: 0, 100, 1).

{
  "id": "clockFontSize",
  "title": "Font Size (vw)",
  "type": "number",
  "default": 2.5,
  "min": 0.5,
  "max": 10,
  "step": 0.1
}
onFieldChange.add('clockFontSize', function(value) {
    document.getElementById('clock').style.fontSize = value + 'vw';
});

Decimal steps

Setting "step": 0.1 or "step": 0.05 enables fine-grained control (e.g., for opacity 0–1 or margin values).


toggle example — basic boolean
{
  "id": "showClock",
  "title": "Show Clock",
  "type": "toggle",
  "default": true
}
onFieldChange.add('showClock', function(value) {
    document.getElementById('clock').style.display = value ? '' : 'none';
});
toggle example — custom string values

Use trueValue and falseValue to emit custom strings instead of true/false. Useful for CSS values.

{
  "id": "fontWeight",
  "title": "Bold text",
  "type": "toggle",
  "default": "bold",
  "trueValue": "bold",
  "falseValue": "normal"
}
onFieldChange.add('fontWeight', function(value) {
    document.getElementById('label').style.fontWeight = value;
});

select example
{
  "id": "clockAnchorClass",
  "title": "Position",
  "type": "select",
  "default": "Top-Right",
  "options": [
    "Top-Left", "Top-Center", "Top-Right",
    "Middle-Left", "Middle-Center", "Middle-Right",
    "Bottom-Left", "Bottom-Center", "Bottom-Right"
  ]
}

Optionally, use optionLabels to show friendlier display names while keeping the option values as-is:

{
  "id": "theme",
  "title": "Color theme",
  "type": "select",
  "default": "dark",
  "options": ["dark", "light", "amoled"],
  "optionLabels": ["Dark", "Light", "AMOLED Black"]
}
onFieldChange.add('theme', function(value) {
    document.body.setAttribute('data-theme', value);
});

color example
{
  "id": "textColor",
  "title": "Text Color",
  "type": "color",
  "default": "#ffffff"
}
onFieldChange.add('textColor', function(value) {
    document.documentElement.style.setProperty('--text-color', value);
});

multitoggle example

A summary row that opens a dialog of named checkboxes. The full state object is dispatched on every checkbox change.

{
  "id": "visibleFields",
  "title": "Visible fields",
  "type": "multitoggle",
  "default": { "cpu": true, "ram": false, "ip": true },
  "options": [
    { "id": "cpu", "title": "CPU load" },
    { "id": "ram", "title": "RAM usage" },
    { "id": "ip",  "title": "Local IP" }
  ]
}
onFieldChange.add('visibleFields', function(state) {
    document.getElementById('cpu').style.display = state.cpu ? '' : 'none';
    document.getElementById('ram').style.display = state.ram ? '' : 'none';
    document.getElementById('ip').style.display  = state.ip  ? '' : 'none';
});

multiselect example

An inline list of checkboxes. Each item can be toggled independently. The callback receives an array of the currently selected values.

{
  "id": "activeMetrics",
  "title": "Active metrics",
  "type": "multiselect",
  "default": ["cpu", "ram"],
  "options": ["cpu", "ram", "ip", "temp"]
}
onFieldChange.add('activeMetrics', function(selected) {
    // selected is an array, e.g. ["cpu", "ip"]
    updateVisibleRows(selected);
});

Helper functions injected by the app

These globals are always available in your widget when Receive system data is enabled:

window.setCssVar(id, value) — Sets a CSS custom property on <body>. The most efficient way to drive styles from config values.

onFieldChange.add('fontSize', function(value) {
    window.setCssVar('fontSize', value);
});
#label {
    font-size: calc(var(--fontSize, 20) * 1px);
}

window.setDataAttr(element, id, value) — Sets a data-* attribute on a DOM element. Useful for CSS state-based styling.

var box = document.getElementById('statusBox');
onFieldChange.add('sys_vpn', function(active) {
    window.setDataAttr(box, 'vpn', active ? 'on' : 'off');
});
#statusBox[data-vpn="on"]  { background: green; }
#statusBox[data-vpn="off"] { background: red;   }

window.debounce(fn, delay) — Returns a debounced version of a function. Useful to throttle rapidly-changing fields before writing to the DOM.

var updateLayout = window.debounce(function(value) {
    document.getElementById('panel').style.width = value + 'px';
}, 100);

onFieldChange.add('panelWidth', updateLayout);

Available sys_* fields

sys_* fields are delivered automatically once per second. You do not declare them in config.json — just register callbacks in your JavaScript using onFieldChange.add().

Display & FPS

Field Description Example value
sys_display_res Screen resolution "1920x1080"
sys_display_fps Screen refresh rate (Hz) "60"
sys_now_fps Video playback FPS "23.97"

CPU

Field Description Example value
sys_cpu_load Total CPU usage percentage "42%"
sys_cpu_temp CPU temperature "65°C"
sys_cpu_governor Active CPU governor "performance"
sys_cpu_clock_0 Core 0 frequency "1300 MHz (62.8%)" — also _1, _2… up to the number of cores

Memory

Field Description Example value
sys_ram_used Used RAM (formatted) "3.20GB"
sys_ram_total Total RAM (formatted) "8.00GB"
sys_ram_free Free RAM (formatted) "4.80GB"
sys_ram_used_kb Used RAM in raw kilobytes 3355648 (integer)
sys_ram_total_kb Total RAM in raw kilobytes 8388608 (integer)

Storage

Field Description Example value
sys_storage_used Used internal storage (formatted) "6.34GB"
sys_storage_total Total internal storage (formatted) "8.57GB"
sys_storage_free Free internal storage (formatted) "2.23GB"
sys_disk_used_bytes Used internal storage in raw bytes integer
sys_disk_total_bytes Total internal storage in raw bytes integer
sys_ext_storage_used Used external storage (formatted) formatted string
sys_ext_storage_total Total external storage (formatted) formatted string
sys_ext_storage_free Free external storage (formatted) formatted string
sys_ext_disk_used_bytes Used external storage in raw bytes integer
sys_ext_disk_total_bytes Total external storage in raw bytes integer

Network & connectivity

Field Description Example value
sys_net Active connection type "WiFi" / "Ethernet" / "Cellular" / "None"
sys_conn_dl Download speed "1.09 KB/s"
sys_conn_ul Upload speed "0.98 KB/s"
sys_vpn VPN active (detected via IpInfoMonitor) Boolean
sys_wifi_signal WiFi RSSI in dBm -21 — value -200 means not connected
sys_audio_route Active audio output "Bluetooth" / "HDMI" / "USB" / "Internal" / "Unknown"

IP addresses

Field Description Example value
sys_ip_public External public IP (fetched every 30 min) "203.0.113.5"
sys_ip_local Local LAN IPv4 "192.168.0.2"
sys_ip_gateway Default gateway "192.168.0.1"
sys_ip_dns DNS servers, comma-separated "94.140.14.14,94.140.15.15"
sys_ip_ipv6 IPv6 addresses, comma-separated string or ""
sys_ip_vpn VPN tunnel IPv4 string or ""
sys_ip_vpn_type VPN interface type "tun" / "wg" / "ppp" or ""
sys_ip_vpn_iface VPN interface name "tun0" or ""

Now Playing (media)

Field Description Example value
sys_now_title Track / video title string or ""
sys_now_artist Artist name string or ""
sys_now_playing Playback state 3 = playing, 2 = paused, other = stopped
sys_now_app Display name of the media app "SmartTube"
sys_now_app_icon Media app icon as base64 PNG data URL "data:image/png;base64,…"
sys_media_position Current playback position "7:22"
sys_media_duration Total duration "11:54"
sys_media_is_live Whether the stream is live Boolean

Foreground app

Field Description Example value
sys_app_name Display name of the foreground app "Projectivity Launcher"
sys_current_app Package name of the foreground app "com.spocky.projengmenu"
sys_app_icon Foreground app icon as base64 PNG data URL "data:image/png;base64,…"
sys_app_memory App memory usage (formatted) "189.6 MB"
sys_app_storage App storage usage (formatted) "210 MB"
sys_active_processes Background process count (-1 = standard mode) 4 or -1

Time, date & weather

Field Description Example value
sys_time Current time (respects user time pattern setting) "12:14 PM"
sys_date Current date (respects user date pattern setting) "13/04/26"
sys_weather_temp Weather temperature "34°C"
sys_weather_icon Weather condition — emoji (wttr.in) or icon URL (other sources) "☀️" or URL string

Complete standalone overlay example

A single index.html (no ZIP needed) that shows time, CPU, RAM and local IP using sys_* fields:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    html, body {
      margin: 0; padding: 0;
      width: 100vw; height: 100vh;
      overflow: hidden;
      background: transparent;
      font-family: monospace;
    }
    #panel {
      position: absolute;
      top: 16px; right: 16px;
      background: rgba(0,0,0,0.75);
      border-radius: 10px;
      padding: 14px 18px;
      color: #fff;
      min-width: 200px;
    }
    .row {
      display: flex;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 6px;
      font-size: 13px;
    }
    .label { color: #888; }
    .value { color: #fff; font-weight: 600; }
  </style>
</head>
<body>
  <div id="panel">
    <div class="row">
      <span class="label">Time</span>
      <span class="value" id="time">--:--</span>
    </div>
    <div class="row">
      <span class="label">CPU</span>
      <span class="value" id="cpu">--%</span>
    </div>
    <div class="row">
      <span class="label">RAM</span>
      <span class="value" id="ram">-- GB</span>
    </div>
    <div class="row">
      <span class="label">IP</span>
      <span class="value" id="ip">---.---.---.---</span>
    </div>
  </div>

  <script>
    window.onFieldChange = window.onFieldChange || {};

    onFieldChange.add('sys_time',     function(v) { document.getElementById('time').textContent = v; });
    onFieldChange.add('sys_cpu_load', function(v) { document.getElementById('cpu').textContent  = v; });
    onFieldChange.add('sys_ram_used', function(v) { document.getElementById('ram').textContent  = v; });
    onFieldChange.add('sys_ip_local', function(v) { document.getElementById('ip').textContent   = v || 'N/A'; });
  </script>
</body>
</html>

Building a multi-widget ZIP with WidgetAnchor

The most powerful way to create widgets is inside a ZIP using the WidgetAnchor base class pattern, as used by the All-in-One Overlay Pack. This lets you build multiple independent widgets in a single overlay, each with its own position, opacity, margin, and visibility — all controllable from config.json.

How WidgetAnchor works

WidgetAnchor is a custom HTML element base class (defined in scripts/widget-anchor.js) that handles absolute positioning and visibility via CSS custom properties. Every widget that extends it automatically gets:

  • Absolute positioning on the screen controlled by a class name (e.g., Top-Right)
  • --visible CSS variable (0 or 1) multiplied with --op for opacity
  • --mx / --my CSS variables for margin in vw / vh

widget-anchor.js (include this in every multi-widget ZIP):

class WidgetAnchor extends HTMLElement {

  static cssInjected = false;

  constructor() {
    super();
    this._position = "Top-Right";
  }

  connectedCallback() {
    if (!WidgetAnchor.cssInjected) {
      const style = document.createElement("style");
      style.textContent = `
        .widget-anchor {
          display: inline-block;
          position: absolute;
          opacity: calc(var(--visible, 1) * var(--op, 1));
          pointer-events: none;
          margin-top:    calc(var(--my, 0) * 1vh);
          margin-bottom: calc(var(--my, 0) * 1vh);
          margin-left:   calc(var(--mx, 0) * 1vw);
          margin-right:  calc(var(--mx, 0) * 1vw);
        }
        .widget-anchor.Top-Left       { top: 0; left: 0; }
        .widget-anchor.Top-Center     { top: 0; left: 50%; transform: translateX(-50%); }
        .widget-anchor.Top-Right      { top: 0; right: 0; }
        .widget-anchor.Middle-Left    { top: 50%; left: 0; transform: translateY(-50%); }
        .widget-anchor.Middle-Center  { top: 50%; left: 50%; transform: translate(-50%, -50%); }
        .widget-anchor.Middle-Right   { top: 50%; right: 0; transform: translateY(-50%); }
        .widget-anchor.Bottom-Left    { bottom: 0; left: 0; }
        .widget-anchor.Bottom-Center  { bottom: 0; left: 50%; transform: translateX(-50%); }
        .widget-anchor.Bottom-Right   { bottom: 0; right: 0; }
      `;
      document.head.appendChild(style);
      WidgetAnchor.cssInjected = true;
    }
    this.classList.add("widget-anchor");
    this._applyPosition();
  }

  onFieldChange(id, value) {
    switch (id) {
      case "visible":  this.style.setProperty("--visible", value ? 1 : 0); return;
      case "position": this._position = value; this._applyPosition(); return;
      case "opacity":  this.style.setProperty("--op", value); return;
      case "marginX":  this.style.setProperty("--mx", value); return;
      case "marginY":  this.style.setProperty("--my", value); return;
    }
  }

  _applyPosition() {
    this.classList.remove(
      "Top-Left", "Top-Center", "Top-Right",
      "Middle-Left", "Middle-Center", "Middle-Right",
      "Bottom-Left", "Bottom-Center", "Bottom-Right"
    );
    this.classList.add(this._position);
  }
}

Available anchor positions

Top-Left, Top-Center, Top-Right, Middle-Left, Middle-Center, Middle-Right, Bottom-Left, Bottom-Center, Bottom-Right


Creating your own widget — step by step

Step 1 — Create scripts/my-widget.js extending WidgetAnchor:

class MyWidget extends WidgetAnchor {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; pointer-events: none; }
        #txt  {
          color: var(--myColor, #fff);
          font-size: calc(var(--mySize, 2) * 1vw);
          font-weight: bold;
        }
      </style>
      <span id="txt">Hello</span>
    `;
    this._txt = this.shadowRoot.querySelector("#txt");
  }

  connectedCallback() {
    super.connectedCallback();
    const d = window.onFieldChange;

    // Wire config fields to WidgetAnchor base class
    d.add("myShow",    v => super.onFieldChange("visible",  v));
    d.add("myAnchor",  v => super.onFieldChange("position", v));
    d.add("myOpacity", v => super.onFieldChange("opacity",  v));
    d.add("myMarginX", v => super.onFieldChange("marginX",  v));
    d.add("myMarginY", v => super.onFieldChange("marginY",  v));

    // Wire config fields to CSS vars (most efficient for style-only changes)
    d.add("myColor",   v => this.style.setProperty("--myColor", v));
    d.add("mySize",    v => this.style.setProperty("--mySize",  v));

    // Wire a sys_* field to the DOM
    d.add("sys_time",  v => { this._txt.textContent = v ?? ""; });
  }
}
customElements.define("my-widget", MyWidget);

Style efficiency

When a field only affects a CSS property, using this.style.setProperty("--my-var", value) is more efficient than re-rendering the template. Reserve full re-renders for structural DOM changes.

Step 2index.html: load widget-anchor.js first, then your widget scripts, and place the custom element tag inside the overlay container:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    html, body {
      margin: 0; padding: 0;
      width: 100vw; height: 100vh;
      overflow: hidden;
      background: transparent;
    }
    #overlay {
      position: absolute;
      inset: 0;
      pointer-events: none;
      z-index: 999999;
    }
  </style>
</head>
<body>
  <div id="overlay">
    <my-widget></my-widget>
  </div>

  <script src="scripts/widget-anchor.js"></script>
  <script src="scripts/my-widget.js"></script>
</body>
</html>

Step 3config.json: add a block for your widget with all its fields. The standard field set for a WidgetAnchor-based widget is:

{
  "title": "My Widget",
  "filename": "my-widget",
  "blocks": [
    {
      "title": "My Widget",
      "fields": [
        {
          "id": "myShow",
          "title": "Show",
          "type": "toggle",
          "default": true
        },
        {
          "id": "myAnchor",
          "title": "Position",
          "type": "select",
          "default": "Top-Right",
          "options": [
            "Top-Left", "Top-Center", "Top-Right",
            "Middle-Left", "Middle-Center", "Middle-Right",
            "Bottom-Left", "Bottom-Center", "Bottom-Right"
          ]
        },
        {
          "id": "myMarginX",
          "title": "Margin X (vw)",
          "type": "number",
          "default": 1,
          "min": 0,
          "max": 20,
          "step": 0.5
        },
        {
          "id": "myMarginY",
          "title": "Margin Y (vh)",
          "type": "number",
          "default": 1,
          "min": 0,
          "max": 20,
          "step": 0.5
        },
        {
          "id": "myOpacity",
          "title": "Opacity",
          "type": "number",
          "default": 1,
          "min": 0,
          "max": 1,
          "step": 0.05
        },
        {
          "id": "myColor",
          "title": "Text Color",
          "type": "color",
          "default": "#ffffff"
        },
        {
          "id": "mySize",
          "title": "Font Size (vw)",
          "type": "number",
          "default": 2,
          "min": 0.5,
          "max": 10,
          "step": 0.1
        }
      ]
    }
  ]
}

Step 4 — Zip the files (without a wrapping folder, or inside one single folder) and load it in tvQuickActions via Select ZIP file.


Community: If you need help or want to share widgets, join t.me/tvquickactions.