ForwardWidget Framework

repository·master·Indexed 42 days ago

https://github.com/inchstudio/forwardwidgets

A JavaScript component framework for building modular web components. It provides specialized APIs for DOM operations via cheerio, HTTP requests, local and shared caching, and a structured data model for video items, playback resources, and Danmu (bullet chat) integration.

Tokens
7K
Snippets
19
Records
20
Agent score
3%

What's inside ForwardWidget

  1. Implement Danmu (Comment) Segment Loading

    master

    ForwardWidget supports on-demand loading of danmu segments for long videos. To use this, declare modules in WidgetMetadata.modules with type: "danmu".

    Required Module IDs

    1. searchDanmu: Search for danmu resources by title.
    2. getCommentsById: Fetch segment information (often from a cache or server).
    3. getDanmuWithSegmentTime: (Optional) Fetch danmu for a specific playback timestamp.

    Supported Response Formats

    Format 1 (Object-based):

    [{ "p": "attributes", "m": "content", "cid": "id" }]

    Format 2 (Array-based):

    [[time, position, color, extra, "content"]]
    // Example Danmu Module Config
    modules: [
      { id: "searchDanmu", title: "Search Danmu", functionName: "searchDanmu", type: "danmu", params: [] },
      { id: "getComments", title: "Get Danmu", functionName: "getCommentsById", type: "danmu", params: [] }
    ]
  2. Implement Danmu (Bullet Chat) modules

    master

    ForwardWidget supports segmented Danmu loading for long videos. To implement this, declare modules in WidgetMetadata.modules with type: "danmu".

    Required Module IDs:

    • searchDanmu: Search for Danmu resources by title.
    • getCommentsById: Fetch Danmu segment info.
    • getDanmuWithSegmentTime: (Optional) Fetch Danmu for a specific timestamp.

    Danmu Loading Flow:

    1. Search (searchDanmu) $\rightarrow$ 2. Fetch Segments (getCommentsById) $\rightarrow$ 3. Match Time (getDanmuWithSegmentTime).

    Supported Response Formats:

    Format 1 (Object-based):

    [
      { p: "time/pos/color", m: "content", cid: "id" }
    ]

    Format 2 (Array-based):

    [
      [0, "position", "#color", "", "content"]
    ]
    // Module configuration
    modules: [
      {
        id: "searchDanmu",
        title: "搜索弹幕",
        functionName: "searchDanmu",
        type: "danmu",
        params: []
      },
      {
        id: "getComments",
        title: "获取弹幕",
        functionName: "getCommentsById",
        type: "danmu",
        params: []
      }
    ]
  3. Implement List and Detail modules

    master

    Widgets typically consist of a list module and a detail module.

    List Modules Declared in WidgetMetadata.modules. The handler function must return an array of VideoItem objects. When a user clicks a category or person in a detail view, the client returns to the list module and passes the relevant ID in params (e.g., params.genreId or params.peopleId).

    Detail Modules loadDetail(link) is a top-level function (not inside modules). It is called by the client using the link property of a VideoItem. It is used to fetch supplementary data like trailers, cast, or related items. It should return the same VideoItem model, potentially with additional fields.

    Forward Resource Matching If an item represents a stable Forward cloud drive resource, set type: "forward" and use the same ID for both id and link. The client will automatically normalize the ID to fw-<id> for resource matching.

    // List module example
    async function loadList(params) {
      return [{
        id: 550,
        type: "tmdb",
        title: "Fight Club",
        mediaType: "movie",
      }];
    }
    
    // Detail module example
    async function loadDetail(link) {
      // Returns supplementary data for the item at 'link'
      return { id: "550", type: "tmdb", title: "Fight Club", ... };
    }
  4. Encrypt JS Modules

    master

    To protect your logic, you can encrypt your JavaScript modules. Encrypted modules are automatically decrypted by the client upon import.

    Encryption Methods

    1. Online Tool: Use the Encryption Tool in your browser.
    2. CLI/API: Use curl to post a file to the encryption endpoint: curl -X POST https://widgetencrypt.inchmade.ai --data-binary @path/to/widget.js -o path/to/widget.js
    3. Claude Code: If using Claude Code in this repository, use the command /fw-encrypt <file_path>.
    curl -X POST https://widgetencrypt.inchmade.ai --data-binary @widgets/tmdb.js -o widgets/tmdb.js
  5. Create a custom Widget using the scaffold

    master

    You can quickly generate a default Widget template by running the scaffold script. This creates a new file in the widgets/ directory containing a default WidgetMetadata object and a loadResource module, with automatic structure validation.

    To generate a default template:

    node scaffold/create-widget.js

    For advanced customization, you can use the --interactive flag or pass specific arguments:

    • --id: Widget ID
    • --title: Widget Title
    • --modules '[...]': Array of module configurations
    • --output widgets: Output directory
  6. Create a new Widget using the Scaffold

    master

    You can quickly generate a default widget template using the provided scaffold script. Running the command without options creates a default widget (ID forward.meta.demo, module loadResource) in the widgets/ directory and validates the metadata.

    To customize the creation, use the --interactive flag or pass specific options like --id, --title, --modules, and --output.

    node scaffold/create-widget.js
    # Or with custom options
    node scaffold/create-widget.js --id "my.custom.widget" --title "My Widget" --output ./my_widgets
  7. Encrypt Widget modules

    master

    To protect your source code, you can encrypt your JS modules. The App will automatically decrypt them upon import.

    Methods to Encrypt:

    1. Online Tool: Use https://forward.vvebo.vip/encrypt/ in your browser.
    2. CLI (via curl):
      curl -X POST https://widgetencrypt.inchmade.ai --data-binary @path/to/widget.js -o path/to/widget.js
    3. Claude Code: If using Claude Code in this repository, use the built-in command:
      /fw-encrypt path/to/widget.js
    curl -X POST https://widgetencrypt.inchmade.ai --data-binary @widgets/tmdb.js -o widgets/tmdb.js
  8. Configure WidgetMetadata

    master

    Every Widget script must begin with a WidgetMetadata object. This object defines the widget's identity, versioning, and its functional modules (which contain the actual logic).

    Key fields include:

    • id: A unique identifier for the widget.
    • modules: An array of module objects. Each module defines a functionName (the handler), params (user-configurable settings), and cacheDuration.
    • search: (Optional) Configuration for search functionality, including a functionName and params array.
    • detailCacheDuration: How long to cache detail data (in seconds).
    var WidgetMetadata = {
        id: "unique_id",
        title: "Widget Title",
        description: "Description",
        author: "Author Name",
        site: "https://example.com",
        version: "1.0.0",
        requiredVersion: "0.0.1",
        detailCacheDuration: 60,
        modules: [
            {
                title: "Module Title",
                description: "Description",
                requiresWebView: false,
                functionName: "functionName",
                sectionMode: false,
                cacheDuration: 3600,
                params: [
                    {
                        name: "paramName",
                        title: "Param Title",
                        type: "input", // input | constant | enumeration | count | page | offset
                        description: "Description",
                        value: "defaultValue",
                        belongTo: {
                            paramName: "param name",
                            value: ["value"]
                        },
                        placeholders: [
                            { title: "Option Title", value: "optionValue" }
                        ],
                        enumOptions: [
                            { title: "Option Title", value: "optionValue" }
                        ]
                    }
                ]
            }
        ],
        search: {
            title: "Search",
            functionName: "search",
            params: []
        }
    };
  9. Handle subtitle archives with `resolveSubtitleArchive`

    master

    If a subtitle is provided as a .zip archive, the client will download and extract it. To select the best subtitle from the extracted files, define a top-level function resolveSubtitleArchive(params). This function is called before the subtitle is used.

    Available params:

    • archiveName: Filename of the archive.
    • archiveUrl: Download URL.
    • subtitleId / subtitleName / subtitleLanguage: Current subtitle context.
    • files: JSON string of all extracted files.
    • subtitleFiles: JSON string of files supported by the client.

    File Object Structure (inside files):

    • path: The relative path within the archive (e.g., Season 1/E02.zh.ass). Use this value for the return.
    • name: Filename.
    • extension: Lowercase extension.
    • size: File size.

    Return Value: A relative path string, a string array, or an object { path: "..." }.

    async function resolveSubtitleArchive(params) {
      const subtitleFiles = JSON.parse(params.subtitleFiles || "[]");
      // Find the first subtitle containing "简体" (Simplified Chinese)
      const best = subtitleFiles.find(file => file.name.includes("简体")) || subtitleFiles[0];
      return best ? best.path : null;
    }
  10. Use the HTTP Request API

    master

    The Widget.http API allows for asynchronous network requests. It supports GET and POST methods.

    GET Request: const response = await Widget.http.get(url, options);

    POST Request: const response = await Widget.http.post(url, body, options);

    Options Object Structure:

    • allow_redirects: boolean
    • headers: Object (e.g., { "User-Agent": "...", "Referer": "..." })
    • params: Object for query parameters

    Access the response body via response.data.

    // GET example
    const response = await Widget.http.get(url, options);
    let data = response.data;
    
    // POST example
    const response = await Widget.http.post(url, body, options);
  11. Implement module handler functions

    master

    Each module in WidgetMetadata.modules must implement a handler function whose name matches the functionName property. The handler is an async function that receives a params object containing all configured parameter values.

    Standard workflow inside a handler:

    1. Validate required parameters.
    2. Perform network requests using Widget.http.
    3. Parse response data using Widget.dom.
    4. Return an array of objects matching the VideoItem data model.
    async function functionName(params = {}) {
      try {
        if (!params.requiredParam) {
          throw new Error("缺少必要参数");
        }
    
        const response = await Widget.http.get(url, {
          headers: {
            "User-Agent": "Mozilla/5.0 ...",
            Referer: "https://example.com",
          },
        });
    
        const docId = Widget.dom.parse(response.data);
        const elements = Widget.dom.select(docId, "selector");
    
        return elements.map((element) => ({
          id: "unique_id",
          type: "type",
          title: "title",
          coverUrl: "url",
        }));
      } catch (error) {
        console.error("处理失败:", error);
        throw error;
      }
    }