# MediaMonkey 5 Addon and Skin Guide

## 1. What You Are Building

MediaMonkey 5 is HTML/CSS/JS-based, with native capabilities exposed through the `app` bridge.

In practice, you usually build one of these:

- Script addon (`type: "general"`, `"metadata"`, `"sync"`, etc.) to add behavior.
- Skin (`type: "skin"`) to change visual style (colors, fonts, icons, etc.).
- Layout (`type: "layout"`) to change structure/arrangement of UI controls.
- Visualization addon (`type: "visualization"`) for playback visuals.

## 2. Package Basics (MMIP)

An `.mmip` is a ZIP archive with a different extension.

Core rules:

- `info.json` must be at package root.
- Files in your package mirror MediaMonkey's internal folder structure.
- Preferred default: extend existing code using `_add` files.
- Advanced option: replace existing files by shipping the same path/name.

Examples from intro doc:

- Extend: `controls/navigationBar_add.js`
- Replace: `controls/navigationBar.js`

## 3. Required and Important Metadata (`info.json`)

At minimum, treat these as essential:

- `title`
- `id`
- `description`
- `version` (`x.y.z` format)

Commonly important optional fields:

- `minAppVersion`
- `type`
- `author`
- `icon`
- `config`
- `updateURL` (for self-hosted updates)
- `installScript` / `uninstallScript`
- `showRestartPrompt`

Important behavior notes:

- Keep `id` stable and unique.
- `version` should follow semantic versioning style where possible.
- `updateURL` enables update checks outside the official addons site.

## 4. Entry Points and File Patterns

Common runtime entry points:

- `init.js` for startup logic in many addon types. It should be used for UI interactions, as 
- `*_add.js` for augmenting existing MM modules.
- `actions_add.js` for custom actions/hotkeys/menu registrations.

Typical usage:

- `info.json` holds package metadata.
- `*_add.js` files implement additive integration.
- `init.js` is used for startup logic where needed.

## 5. Extend vs Replace Strategy

Prefer additive extension first:

- It is safer across MediaMonkey updates.
- It composes better with other addons.
- It keeps your diff focused on your feature.

Use full replacement only when extension points are insufficient.

## 6. Common Customization Areas

Most addon work clusters into these areas:

- Actions and hotkeys.
- Main and context menus.
- Toolbar buttons/icons.
- Tree nodes and navigation/view handlers.
- Player behavior and now-playing interactions.
- Options/config UI and persistent settings.

## 7. Data and Persistence

For simple persistent settings:

- Use `app.setValue` / `app.getValue` with unique keys (include addon ID prefix).

For larger/relational data:

- Use `app.db.executeQueryAsync` and `app.db.getQueryResultAsync`.

## 8. Actions, Menus, and Hotkeys Lifecycle

- Define custom actions in `actions_add.js`.
- Register hotkeys via `hotkeys.addHotkey(...)`.
- Add menu entries using menu item structures (`action`, `order`, `grouporder`, optional `submenu`).

## 9. Skin and Layout Workflow

Skin/layout work follows the same package mechanics but targets different assets:

- Skin focus: LESS/CSS/SVG/icon and visual look.
- Layout focus: control composition, dimensions, structure, placements.

Both are implemented through file structure mirroring and selective override/extension.

## 10. Development Practices That Prevent Problems

Key recommendations:

- Minimize synchronous startup work.
- Use `window.whenReady(...)` for startup initialization timing. Prefer `init.js` to be sure that the rest of MediaMonkey is fully initialized. Otherwise e.g. access to main window elements (like toolbars) would fail.
- Prefer async workflows for heavy tasks.
- Keep addon code scoped (avoid polluting global namespace).
- Avoid excessive logging in production builds.
- Prefer MM-safe timing wrappers (`requestFrame`, `requestTimeout`) when available.
- Use Developer Mode only for development, not in published builds.

# Tree, Menu, and Toolbar Extension Reference

This consolidated guide merges practical MediaMonkey addon patterns for:

- Creating and extending tree nodes
- Extending main menus and local/context popup menus
- Adding global and node-specific toolbar buttons
- Reusing actions and hotkeys across tree/menu/toolbar surfaces
- Navigating and refreshing node lifecycle state

## 1) Core Architecture: `nodeHandlers` + `actions`

Two concepts drive most UI customization:

- `nodeHandlers` define tree structure, view binding, and node-level behaviors. They are stored in `viewHandlers.js`
- `actions.*` define executable commands reused by menu items, toolbar buttons, and hotkeys

Action shape used across UI surfaces:

```js
actions.myAction = {
  title: _('My Action') + '...',
  icon: 'listview',
  hotkeyAble: true,
  disabled: uitools.notMediaListSelected,
  visible: window.uitools.getCanEdit,
  execute: async function () {
    // do work
  }
};
```

Why this matters:

- The same action can be reused across menu, toolbar, and hotkey entry points
- Runtime guards (`visible`, `disabled`) keep UI relevant and safe

## 2) Tree Node Creation and Structure

### 2.1 Core model: `nodeHandlers` define tree behavior

`nodeHandlers` is the main dictionary describing nodes.

```js
nodeHandlers.myHandler = inheritNodeHandler('MyHandlerClass', 'Base', {
  title: 'My Node',
  icon: 'folder',
  hasChildren: true,
  getChildren(node) {
    return Promise.resolve();
  },
  viewAs: ['nodeList']
});
```

Useful handler members:

- `title` / `setTitle`
- `icon`
- `hasChildren`, `hasTreeChildren`
- `getChildren(node)`
- `viewAs`
- `getViewDataSource(view)`
- `menu`, `hideDefaultMenu`
- `toolbarActions`
- `formatStatus`
- Optional drag/drop/delete behavior (`canDrop`, `drop`, `canDelete`, etc.)

### 2.2 Add a new root-level node by overriding parent

```js
nodeHandlers.root.override({
  getChildren: function ($super, node) {
    return new Promise((resolve) => {
      $super(node).then(function () {
        node.addChild(null, 'myCustomNodesHandler');
        nodeHandlers.root.sortNodesAndVisibility(node);
        resolve();
      });
    });
  }
});
```

Notes:

- Call `$super(node)` first to preserve built-in children
- Use `node.addChild(dataSource, handlerID)` to insert custom children
- Re-sort/re-evaluate visibility when augmenting existing branches (`sortNodesAndVisibility`)

### 2.3 Define a hierarchical custom tree

Step 1: define a shared base handler:

```js
nodeHandlers.myCustomNodesBase = inheritNodeHandler('MyCustomNodesBase', 'Base', {
  menu: function (node) { return [/* actions */]; },
  hideDefaultMenu: true
});
```

Step 2: define parent branch:

```js
nodeHandlers.myCustomNodesHandler = inheritNodeHandler('MyCustomNodesHandler', 'MyCustomNodesBase', {
  title: 'Custom Nodes',
  icon: 'myCustomIcon',
  hasChildren: true,
  getChildren(node) {
    node.addChild(null, 'myTracksNodeHandler');
    node.addChild(null, 'myAlbumsNodeHandler');
    return Promise.resolve();
  },
  viewAs: ['nodeList']
});
```

Step 3: define leaf handlers and bind data:

```js
nodeHandlers.myTracklistSubNodeHandler = inheritNodeHandler('MyTracklistSubNodeHandler', 'MyCustomNodesBase', {
  hasChildren: false,
  viewAs: ['tracklist'],
  getViewDataSource(view) {
    return view.viewNode.dataSource.getTracklist();
  }
});
```

### 2.4 `dataSource`-driven child node pattern

```js
let dataSource = {
  id: item.name,
  title: item.name,
  getTracklist: function () {
    return app.db.getTracklist(item.sql, -1);
  }
};
node.addChild(dataSource, 'myTracklistSubNodeHandler');
```

Benefits:

- Keeps handlers generic while behavior varies by `dataSource`
- Works for different content types (`tracklist`, `albumlist`, etc.)

### 2.5 Dynamic nodes from external services

Service root contributes children:

```js
addCustomNodes: function (node) {
  node.addChild(node.dataSource, 'cloudPlaylists');
  node.addChild(node.dataSource, 'deezerFavoriteArtists');
  node.addChild(node.dataSource, 'deezerFavoriteAlbums');
  node.addChild(node.dataSource, 'cloudTracks');
}
```

Handler fetches remote items and adds one child per item:

```js
getChildren: function (node) {
  return service.listFavArtists().then(function (list) {
    for (var i = 0; i < list.count; i++) {
      node.addChild(list.getValue(i), 'deezerFavoriteArtist');
    }
  });
}
```

This is a standard lazy, network-backed tree pattern.

### 2.6 View binding: `viewAs` + `getViewDataSource`

Common patterns:

- `viewAs: ['nodeList']` for branch-like nodes
- `viewAs: ['tracklist']` with `getViewDataSource` returning a tracklist
- `viewAs: ['albumlist']` with `getViewDataSource` returning an album list
- Multi-view configurations, for example `['tracklist', 'groupedTracklist', 'albumlist']`

Node `viewAs` values map to handlers in `viewHandlers`.

## 3) Extend Existing Nodes Without New Handlers

You can extend built-in nodes directly.

### 3.1 Add context menu entries

```js
nodeHandlers.playlist.menuAddons = nodeHandlers.playlist.menuAddons || [];
nodeHandlers.playlist.menuAddons.push(function (node) {
  return [{ action: someAction, order: 40, grouporder: 10 }];
});
```

### 3.2 Add toolbar actions

```js
nodeHandlers.subscriptions.toolbarActions.push(actions.exportPodcasts);
```

### 3.3 Add static menu addons object

```js
nodeHandlers.subscriptions.menuAddons = nodeHandlers.subscriptions.menuAddons || [];
nodeHandlers.subscriptions.menuAddons.push({ action: actions.exportPodcasts, order: 90, grouporder: 10 });
```

These are extension patterns around existing nodes rather than full node creation, but commonly used with custom trees.

## 4) Main Menu Extensions

In `init.js`, when the main window is already initialized:

### 4.1 Add items via `window._menuItems`

```js
window._menuItems.tools.action.submenu.push({
  action: actions.myAction,
  order: 10,
  grouporder: 20
});
```

Common branches:

- `window._menuItems.tools.action.submenu`
- `window._menuItems.editTags.action.submenu`
- `window._menuItems.reports.action.submenu`
- `window._menuItems.export.action.submenu`
- `window._menuItems.podcasts.action.submenu`

Direct action push variant:

```js
window._menuItems.podcasts.action.submenu.push(actions.exportPodcasts);
```

### 4.2 Ordering fields

Wrapper objects frequently use:

- `order`: item order inside group
- `grouporder`: group placement relative to other groups

These are used heavily in main and context menus.

### 4.3 Extend dynamic submenu providers

```js
var __origSubmenu = actions.sendTo.submenu;
actions.sendTo.submenu = (params) => {
  return new Promise((resolve) => {
    __origSubmenu(params).then((res) => {
      res.push({
        action: actions.burn2MM4,
        grouporder: 40
      });
      resolve(res);
    });
  });
};
```

Guidelines:

- Keep original provider (`__origSubmenu`) and append entries
- Return the same type (`Promise` here)

## 5) Local/Context Popup Menu Extensions

### 5.1 `addToContextMenu(...)`

Return menu items from callback:

```js
el.controlClass.addToContextMenu(function () {
  return [{
    action: {
      title: 'Enable/Disable feature',
      execute: function () {},
      visible: function () { return true; }
    },
    order: 10,
    grouporder: -10
  }];
});
```

Static array with dynamic submenu:

```js
control.addToContextMenu([{
  action: {
    title: 'Select preset',
    submenu: function () {
      return [
        { action: { title: 'Preset A', execute() {} }, order: 1, grouporder: 10 }
      ];
    }
  },
  order: 10,
  grouporder: 10
}]);
```

`submenu` can return:

- An array of menu entries
- A `Promise` resolving to that array

### 5.2 Assign `contextMenu` or implement `getContextMenuItems`

Assign ready `Menu`:

```js
toolbarIcon.controlClass.contextMenu = new Menu([
  { title: 'Soft reload', execute: () => actions.lessReload.execute() },
  { title: 'Quick window reload', execute: () => actions.windowReload.execute() }
]);
```

Generate dynamically:

```js
control.controlClass.contextMenu = []; // initialize
control.controlClass.getContextMenuItems = function () {
  var menuItems = [];
  // populate dynamically
  return menuItems;
};
```

Notes:

- `contextMenu` can be assigned directly or be a function returning a `Menu`
- `getContextMenuItems(evt)` is useful when content depends on runtime state

### 5.3 Specialized local menu extension points

Playing list popup:

```js
window.playingListMenuItems.push({
  order: 10,
  grouporder: 999,
  action: {
    title: 'Undock',
    execute: actions.undockPlaying.execute,
    visible: window.isMainWindow
  }
});
```

Node popup override via `$super`:

```js
nodeHandlersClasses.PlaylistBase.prototype.override({
  menu: function ($super, node) {
    var menuItems = $super(node);
    menuItems.push({
      action: bindAction(actions.undockPlaylist, () => node),
      order: 50,
      grouporder: 10
    });
    return menuItems;
  }
});
```

Visualization menu registry:

```js
window.uitools.visMenuItems = window.uitools.VisMenuItems || {};
window.uitools.visMenuItems['Vis_milkshake'] = [{
  title: _('Select preset'),
  submenu: getPresetMenuItems
}];

// cleanup
window.uitools.visMenuItems['Vis_milkshake'] = undefined;
```

Tracklist shortcut popup builder returning `Menu`:

```js
TrackListView.prototype.getShortcutContextMenu = (parent, column) => {
  var items = [
    {
      action: {
        title: 'Open link',
        execute: function () {
          // use focused item
        }
      },
      grouporder: 10
    }
  ];
  return new Menu(items, { parent: parent });
};
```

## 6) Toolbar Extensions

In `init.js`, when the main window is already initialized:

### 6.1 Global toolbar insertion: `window.uitools.addToolButton(...)`

```js
window.uitools.addToolButton(
  'righttoolbuttons',
  'sql',
  function () {
    uitools.openDialog('dlgSQLEditor', { show: true, notShared: true, title: 'SQL Editor' });
  },
  'SQL Editor'
);
```

Signature:

```js
window.uitools.addToolButton(containerId, iconName, onClick, tooltip)
```

Typical params:

- `containerId`: `'righttoolbuttons'`
- `iconName`: `'sql'`, `'randomAlbumPlay'`, `'showCurrentTrack'`, `'refresh-skin'`
- `onClick`: callback function
- `tooltip`: short description text

### 6.2 Keep returned handle for updates/removal

`addToolButton` returns the created element/control.

```js
var toolbarIcon;

if (toolbarIcon) {
  qid('righttoolbuttons').removeChild(toolbarIcon);
  toolbarIcon = undefined;
}

toolbarIcon = window.uitools.addToolButton('righttoolbuttons', 'refresh-skin', onClick, tooltip);
```

Use cases:

- Show/hide based on settings
- Recreate button with changed tooltip or behavior
- Attach extra behavior (`contextMenu`, dynamic disabled state)

### 6.3 Add context menu to toolbar icon

```js
toolbarIcon.controlClass.contextMenu = new Menu([
  { title: 'Soft reload', icon: 'rightArrow', execute: () => actions.lessReload.execute() },
  { title: 'Quick window reload', icon: 'next', execute: () => actions.windowReload.execute() },
  { title: 'Full app reload', icon: 'refresh', execute: () => actions.fullAppReload.execute() }
]);
```

### 6.4 Node-specific toolbar actions

```js
nodeHandlers.subscriptions.toolbarActions.push(actions.exportPodcasts);
```

Action includes icon:

```js
actions.exportPodcasts = {
  title: _('Export subscribed podcasts') + '...',
  icon: 'export',
  execute: function () {
    // ...
  }
};
```

Guideline:

- Define action with `icon`
- Push action into relevant `nodeHandlers.<node>.toolbarActions`

### 6.5 Build custom toolbar-like buttons in DOM

Declarative icon via `data-icon`:

```js
var button = document.createElement('div');
button.classList.add('noPadding', 'lvHeaderItem');
button.innerHTML =
  '<div data-id="unDockBtn" data-icon="mode_windowed" data-tip="Undock" ' +
  'class="menuButton toolbutton" data-control-class="ToolButton"></div>';
UI.header.appendChild(button);
initializeControl(qeid(button, 'unDockBtn'));
```

SVG icon via `loadIcon(...)`:

```js
let div = document.createElement('div');
div.classList.add('toolbutton', 'paddingSmall');
div.setAttribute('data-tip', _('Undock'));
loadIcon('mode_windowed', (iconData) => {
  div.innerHTML = iconData;
});
this.localListen(div, 'click', () => actions.undockPlayingNode.execute());
this.switcherDiv.appendChild(div);
```

Extend navigation toolbar manually:

```js
this._redoBtn = document.createElement('div');
this._redoBtn.setAttribute('data-icon', 'redo');
this._redoBtn.setAttribute('data-control-class', 'Control');
this._redoBtn.className = 'toolButton';
this.navContainer.appendChild(this._redoBtn);
initializeControl(this._redoBtn);
```

Related extras:

- `tooltipValueCallback` for dynamic tooltip text
- long-click/right-click context menu (`contextMenuHandler`)
- dynamic enable/disable (`controlClass.disabled`)

## 7) Shared Menu/Toolbar Item Shapes

Wrapped shape (`action` + ordering):

```js
{
  action: {
    title: 'My item',
    icon: 'icon_name',
    execute: function () {},
    visible: function () { return true; },
    checked: function () { return false; },
    submenu: function () { return []; }
  },
  order: 10,
  grouporder: 10
}
```

Flat shape in custom builders (`new Menu([...])`):

```js
{
  title: 'My item',
  icon: 'icon_name',
  execute: function () {},
  visible: function () { return true; },
  checked: function () { return false; }
}
```

Both styles are valid depending on API surface.

## 8) Node Lifecycle and Runtime Utilities

Create standalone runtime node:

```js
var node = app.createTree().createNode();
node.handlerID = 'npview';
```

Clone handler/dataSource from another node:

```js
var node = app.createTree().createNode();
node.handlerID = fromNode.handlerID;
node.dataSource = fromNode.dataSource;
```

Use case:

- Open a view/dialog with synthetic node (`createViewData({ node })`) without inserting into main tree

Refresh/navigate utilities:

- `nodeUtils.refreshNodeChildren(node)` after config changes
- `navUtils.createNodeState(handlerID, dataSource?)`
- `navUtils.navigateNodePath([...states])`

Navigation pattern:

```js
navUtils.navigateNodePath([
  navUtils.createNodeState('root'),
  navUtils.createNodeState('devicesList'),
  navUtils.createNodeState('device', device)
]);
```

## 9) Node Handler Contracts and Integration Rules

`viewHandlers.nodeHandlers` includes explicit menu/toolbar contract fields:

- `menu: []` for custom popup menu items
- `hideDefaultMenu: true` to prevent default popup items from being appended
- `toolbarActions` entries require actions containing `identifier`

Practical implication:

- In `nodeHandlersClasses.*.prototype.override({ menu(...) { ... } })`, use `hideDefaultMenu` when fully replacing defaults

## 10) Hotkeys Integration (Action + Tree/Menu/Toolbar)

Define hotkey-capable action:

```js
actions.myCustomAction = {
  title: _('My Custom Action'),
  hotkeyAble: true,
  execute: function () {
    // work
  }
};
```

Register shortcuts:

```js
hotkeys.addHotkey('Ctrl+Shift+Q', 'myCustomAction');
hotkeys.addHotkey('Ctrl+Shift+D', 'burn2MM4');
window.hotkeys.addHotkey('Shift+F5', 'lessReload', false);
```

Notes:

- Signature: `hotkeys.addHotkey(hotkey, action, global)`
- `action` is action ID/name in `window.actions`
- `global` controls outside-window behavior (optional)
- Duplicate/deletion handling is built in; user hotkey changes are respected
- Both `hotkeys.addHotkey(...)` and `window.hotkeys.addHotkey(...)` are common

Keep invocation paths unified:

1. Define action in `actions_add.js`
2. Bind action to tree menu/toolbars/main menu as needed
3. Register hotkey for the same action ID

## 11) UX, Accessibility, and Robustness

- If execution opens heavy dialogs/file pickers, defer with `requestAnimationFrameMM(...)` for better menu-close behavior
- For dynamic submenu providers, returning `Promise<menuItems>` is first-class
- Compose with original submenu/provider logic when extending behavior
- Clean up injected registries (for example, `window.uitools.visMenuItems[...] = undefined` on teardown)
- Tooltip via `addToolButton` argument or `data-tip`
- For accessibility on custom controls, use `data-aria-label`

Example:

```js
btn.setAttribute('data-aria-label', _('&Redo').replace('&', ''));
```

## 12) Recommended Strategy

Order of preference:

1. Define reusable `actions.*` first
2. For new navigation areas, define `nodeHandlers` with `viewAs` and `getChildren`
3. For existing-node augmentation, prefer `menuAddons`, `toolbarActions`, and safe overrides with `$super`
4. For global commands, use `window._menuItems.<branch>.action.submenu` and `window.uitools.addToolButton(...)`
5. For local UI surfaces, use `addToContextMenu(...)` or `getContextMenuItems`
6. If no hook exists, build custom DOM controls and call `initializeControl(...)`
7. Use lifecycle utilities (`refreshNodeChildren`, `navigateNodePath`) after runtime changes

## 13) Practical Recipes

### 13.1 Add a new top-level custom branch

```js
// 1) Register handler
nodeHandlers.myBranch = inheritNodeHandler('MyBranch', 'Base', {
  title: 'My Branch',
  icon: 'folder',
  hasChildren: true,
  getChildren(node) {
    node.addChild({ title: 'Child A' }, 'myLeaf');
    return Promise.resolve();
  },
  viewAs: ['nodeList']
});

nodeHandlers.myLeaf = inheritNodeHandler('MyLeaf', 'Base', {
  title: (node) => node.dataSource.title,
  icon: 'listview',
  hasChildren: false,
  viewAs: ['tracklist'],
  getViewDataSource(view) {
    return app.getAllTracksList();
  }
});

// 2) Insert under root
nodeHandlers.root.override({
  getChildren: function ($super, node) {
    return $super(node).then(function () {
      node.addChild(null, 'myBranch');
      nodeHandlers.root.sortNodesAndVisibility(node);
    });
  }
});
```

### 13.2 Add a new main menu item under Tools

```js
actions.myTool = {
  title: _('My Tool') + '...',
  icon: 'build',
  execute: function () {
    uitools.openDialog('dlgMyTool');
  }
};

window._menuItems.tools.action.submenu.push({
  action: actions.myTool,
  order: 30,
  grouporder: 20
});
```

### 13.3 Add a right-click item to a control

```js
myControl.controlClass.addToContextMenu(function () {
  return [{
    action: {
      title: 'Do something',
      execute: function () {
        // action
      }
    },
    order: 10,
    grouporder: 10
  }];
});
```

### 13.4 Add async submenu entries

```js
myControl.controlClass.addToContextMenu([{
  action: {
    title: 'Choose item',
    submenu: function () {
      return fetchItemsAsync().then((items) =>
        items.map((it, idx) => ({
          action: { title: it.title, execute: () => useItem(it) },
          order: idx + 1,
          grouporder: 10
        }))
      );
    }
  }
}]);
```

### 13.5 Template A: simple global toolbar icon

```js
(function () {
  window.uitools.addToolButton('righttoolbuttons', 'my-icon', function () {
    actions.myAction.execute();
  }, 'My action tooltip');
})();
```

### 13.6 Template B: toggleable toolbar icon with saved settings

```js
(function () {
  var toolbarIcon;
  var settings = app.getValue('myAddonSettings', { showToolbarIcon: true });

  function applyToolbarVisibility() {
    if (toolbarIcon) {
      qid('righttoolbuttons').removeChild(toolbarIcon);
      toolbarIcon = undefined;
    }
    if (!settings.showToolbarIcon)
      return;

    toolbarIcon = window.uitools.addToolButton('righttoolbuttons', 'my-icon', function () {
      actions.myAction.execute();
    }, 'My action');
  }

  applyToolbarVisibility();
})();
```

### 13.7 Template C: node-specific toolbar action

```js
actions.myNodeAction = {
  title: _('My node action'),
  icon: 'my-icon',
  execute: function () {}
};

nodeHandlers.myNode.toolbarActions.push(actions.myNodeAction);
```

## 14) Common Pitfalls

- Forgetting `$super(node)` in overridden `getChildren` can hide built-in nodes
- Missing `hasChildren: true` on branch handlers can block expansion behavior
- Missing `viewAs` or `getViewDataSource` on leaf handlers causes empty/incorrect center views
- If menu should include only custom entries, set `hideDefaultMenu: true` intentionally
- For config-driven node sets, call `nodeUtils.refreshNodeChildren(node)` after changes

## 15) Quick Checklist

- Define actions in `actions_add.js` when they should be menu/toolbar/hotkey reusable
- Define handler(s) via `inheritNodeHandler`
- Set `title`, `icon`, `viewAs`, `hasChildren` correctly
- Add children with `node.addChild(dataSource, handlerID)`
- When augmenting built-in nodes, override parent `getChildren` and call `$super`
- Use ordering fields (`order`, `grouporder`) consistently
- Register hotkeys for shared actions
- Refresh and navigate UI after runtime node changes


# app.player and Visualization Addons JS API Reference

## Access

`app.player` is the native player bridge exposed to Chromium/JS addons.

```js
const player = app.player;
```

## Visualization Addon Registration

A visualization package uses `type: "visualization"` in `info.json`.

`init.js` registers one or more visualizers by pushing into `visualizations`:

```js
visualizations.push({
	visName: 'Basic bars',
	className: 'Vis_bars',
	jsFile: 'vis_bars/vis_bars'
});
```

Registration can include optional fields such as `ownWindow`, `classParams`, and `showConfig` for advanced visualizer setups.

## Events (listen via `app.listen`)

Use `app.listen(app.player, '<eventName>', callback)`.

```js
const unlisten = app.listen(app.player, 'playbackState', (newState) => {
	// Possible values of newState are: 'unpause' | 'play' | 'stop' | 'pause' | 'trackChanged' | 'listReordered'
});

// later
app.unlisten(app.player, 'playbackState', unlisten);
```

Available player events:

- `playbackState(newState)`
- `htmlPlaybackState(newState, positionMS)`
- `repeatChange(value)`
- `shuffleChange(value)`
- `stopAfterCurrentChange(value)`
- `seekChange(value)`
- `layoutModified()`
- `nowPlayingModified()`
- `trackSkipped()`
- `playbackEnd()`

## Core Properties

Read/write:

- `volume: float` (0..1)
- `panning: float` (-1..1)
- `mute: boolean`
- `normalize: boolean`
- `repeatPlaylist: boolean`
- `repeatOne: boolean`
- `shufflePlaylist: boolean`
- `crossfade: boolean`
- `stopAfterCurrentTrack: boolean`
- `autoResetStopAfterCurrent: boolean`
- `speed: float` (typically 0.5..2)
- `pitchChange: float` (semitones, typically -12..+12)

Read-only (from JS perspective):

- `playlistPos: integer`
- `isPlaying: boolean`
- `paused: boolean`
- `bitrate: integer`
- `sampleRate: integer`
- `channels: integer`
- `trackLengthMS: integer`
- `trackPositionMS: integer`
- `entriesCount: integer`
- `visualization: Visualization`
- `htmlPlaybackState: HTMLPlaybackState`
- `autoDJ: AutoDJSettings`

Useful playback state properties for visualizers:

- `app.player.isPlaying`
- `app.player.paused`
- `app.player.trackPositionMS`
- `app.player.trackLengthMS`

## Playback Methods

- `playAsync(seekPos?: integer): Promise<any>`
- `stopAsync(): Promise<any>`
- `nextAsync(checkSkip?: boolean, cannotPlayCurrent?: boolean): Promise<any>`
- `prevAsync(): Promise<any>`
- `pauseAsync(): Promise<any>`
- `playPauseAsync(): Promise<any>`
- `seekMSAsync(value: integer): Promise<any>`

Example:

```js
await app.player.playAsync();
await app.player.seekMSAsync(30_000);
```

## Now Playing List Management

- `clearPlaylistAsync(saveHistory?: boolean): Promise<void>`
- `addTracksAsync(tracks, params?): Promise<void>`
- `addTrackAtPos(SD: Track, position: integer): void`
- `removeSelectedTracksAsync(selTracks: Tracklist): Promise<void>`
- `removeNotAccessibleAsync(): Promise<any>`
- `removeDuplicatesAsync(): Promise<any>`
- `reversePlaylistAsync(): Promise<any>`
- `randomizePlaylistAsync(...params: any[]): Promise<any>`

`addTracksAsync` accepts:

- `tracks: Tracklist | StringList | Track`
- `params.withClear?: boolean`
- `params.forcePreview?: boolean`
- `params.saveHistory?: boolean`
- `params.afterCurrent?: boolean`
- `params.position?: integer` (`-1` = end)
- `params.startPlayback?: boolean`
- `params.shuffle?: boolean`
- `params.mix?: boolean`
- `params.byAlbum?: boolean`
- `params.focusedTrackIndex?: integer`

Example:

```js
await app.player.addTracksAsync(tracklist, {
	afterCurrent: true,
	startPlayback: true,
	saveHistory: true
});
```

## Navigation and Track Access

- `getCurrentTrack(): Track` Returns the currently playing track
- `getNextTrack(): Track`
- `getTracklist(): Tracklist`
- `getSongList(): PlaylistEntries`
- `setPlaylistPosAsync(idx: integer): Promise<any>`
- `setPlaylistPosByEntryAsync(pe: PlaylistEntry): Promise<any>`
- `isPlayingTrack(SD: Track): boolean`
- `getIndexOfPlayingTrack(tracklist: Tracklist): integer`
- `getCountOfPlayedEntries(): integer`

There's also `getFastCurrentTrack(track: Track): Track | undefined` which reuses/fills an existing JS 'track' object. The provided object must be of Track type from a previous API call that returns such an object! The track object parameter is required in this function! Use correctly and unless needed for performance reasons, use getCurrentTrack() instead.

## Undo/Redo for Now Playing

- `undoAsync(count: integer): Promise<any>`
- `redoAsync(count: integer): Promise<any>`
- `getUndoList(onlyFirst: boolean): StringList`
- `getRedoList(onlyFirst: boolean): StringList`
- `clearUndoList(): void`

## Visualization Engine API (`app.player.visualization`)

Core members:

- `active: boolean` to enable/disable engine
- `registerVisTypes(types)` to request computed data
- `unregisterVisTypes(types)` to release data requirements
- `spectrumSections: number` (max 20) controls spectrum bucket count
- `getNextResult(fromtime, obj)` for manual polling flow
- `clear()` to clear computed visualization data

`registerVisTypes` values:

- `'spectrum'`
- `'vumeter'`
- `'oscilloscope'`
- `'fft'`

## Visualization Result Data (`VisResult`)

`onUpdateHandler(visData)` receives a `VisResult` object.

Useful scalar fields:

- `timestamp: integer`
- `maxLeft: integer` (0..32768)
- `maxRight: integer` (0..32768)

Useful array getters:

- `getSpectrumValues(obj): any` (up to 20 values)
- `getOscValues(obj): any` (up to 512 int values)
- `getNormalizedOscValues(obj): any` (up to 512 doubles in `-1..1`)
- `getNormalizedFFTValues(obj): any` (up to 512 normalized values)

Performance note:

- Pass your previous array/object to getter calls (`obj`) to reduce allocations
- Reusing buffers is recommended (`this._spectData`, `this._oscData`, `musicData._waveData`, etc.)

Example:

```js
onUpdateHandler(visData) {
	this._wave = visData.getNormalizedOscValues(this._wave);
	const wave = new Float64Array(this._wave);
	// draw wave...
}
```

## Visualization Control Base and Lifecycle

Visualizers commonly inherit from `BaseVis` (`requirejs('controls/baseVis')`) and implement these key callbacks:

- `onResizeHandler(w, h)`: update canvas/resources when parent size changes
- `onUpdateHandler(visData)`: per-frame render callback with visualization data
- `onStopHandler()`: called when playback is stopped or paused
- `onPaintTitle(title, artist)`: commonly used to show temporary title overlays

Typical pattern:

1. `initialize(...)`: create canvases, initialize internal state, register required data types
2. `onResizeHandler(...)`: recalculate geometry and contexts
3. `onUpdateHandler(visData)`: read data from `visData`, draw frame
4. `onStopHandler()`: clear visuals and reset runtime state
5. `cleanUp()`: unregister data types, release DOM/resources, call `super.cleanUp()`

## Data Shapes and Channel Handling

Common usage patterns:

- VU-style visualizers read `maxLeft` and `maxRight` only
- Bar-style visualizers use `getSpectrumValues(...)` and map buckets to bar heights
- Oscilloscope-style visualizers use `getOscValues(...)` and render a polyline
- Advanced engines often use normalized arrays and transform them to their own signal model

Channel splitting pattern:

```js
const data = new Float64Array(visData.getNormalizedOscValues(prev));
if (data.length === 1024) {
	left = data.slice(0, 512);
	right = data.slice(512);
} else {
	left = right = data;
}
```

Why this matters:

- Maximum lengths are documented, but real runtime payload may differ by backend/engine mode
- Defensive length checks make your visualizer robust

## Playback-State Integration for Visualizers

There are two layers to consider.

### BaseVis-level behavior

`BaseVis.onStopHandler()` is invoked when playback is stopped or paused. Use it to:

- clear buffers/canvas
- stop timers or animations
- reset decay/inertia state

A robust implementation should clear rendering and reset runtime state in `onStopHandler()`.

### Player-level events and state

Use `app.listen(app.player, eventName, callback)` to react to transitions.

Visualization-relevant events:

- `playbackState(newState)`
- `htmlPlaybackState(newState, positionMS)`
- `playbackEnd()`
- `trackSkipped()`

Example:

```js
const stopListening = app.listen(app.player, 'playbackState', (state) => {
	if (state === 'trackChanged') {
		const t = app.player.getCurrentTrack();
		if (t) this.onPaintTitle(t.title, t.artistName || t.artist || '');
	}
});

// later in cleanup:
app.unlisten(app.player, 'playbackState', stopListening);
```

## Pull Model vs Callback Model

Most visualizers use callback model (`onUpdateHandler(visData)`).

For custom loops, `Visualization.getNextResult(fromtime, obj)` supports manual polling:

```js
const vis = app.player.visualization;
vis.active = true;
vis.registerVisTypes(['vumeter', 'fft']);

let cache = null;
const tick = () => {
	cache = vis.getNextResult(app.player.trackPositionMS, cache);
	if (cache) {
		const fft = cache.getNormalizedFFTValues([]);
		// render
	}
	requestAnimationFrame(tick);
};
tick();
```

Use one model consistently to avoid duplicated work.

## Minimal Robust Visualizer Skeleton

```js
requirejs('controls/baseVis');

class Vis_example extends BaseVis {
	initialize(parentel, params) {
		super.initialize(parentel, params);
		this.canvas = document.createElement('canvas');
		this.ctx = null;
		this.container.appendChild(this.canvas);

		app.player.visualization.spectrumSections = 20;
		app.player.visualization.registerVisTypes(['spectrum']);
	}

	onResizeHandler(w, h) {
		if (this.canvas.width === w && this.canvas.height === h) return;
		this.canvas.width = w;
		this.canvas.height = h;
		this.ctx = this.canvas.getContext('2d');
	}

	onUpdateHandler(visData) {
		if (!this.ctx) this.onResizeHandler(this._width, this._height);
		this._spec = visData.getSpectrumValues(this._spec);

		this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
		// draw from this._spec
	}

	onStopHandler() {
		if (this.ctx) this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
	}

	cleanUp() {
		app.player.visualization.unregisterVisTypes(['spectrum']);
		if (this.canvas && this.canvas.parentNode === this.container)
			this.container.removeChild(this.canvas);
		super.cleanUp();
	}
}

registerClass(Vis_example);
```

## AutoDJ API

`app.player.autoDJ` (`AutoDJSettings`):

- `enabled: boolean`
- `addTrackCount: integer`
- `keepTrackCount: integer`
- `playlistID: integer`
- `collectionID: integer`
- `source: string`

## HTML Playback State

`app.player.htmlPlaybackState` (`HTMLPlaybackState`):

- `positionMS: integer`
- `lengthMS: integer`
- `state: string`
- `volume: float`

## Plugin and DSP Methods

- `getOutputPlugins(): PluginList`
- `setOutputPlugin(plugin: WAOutputPlugin): void`
- `outputPluginConfig(): void`
- `getInputPlugins(): PluginList`
- `inputPluginConfig(): void`
- `getOutputPluginFeatures(): any`
- `getDSPModules(): PluginList`
- `setDSPModules(mods: PluginList): void`

## Waveform and Analysis

- `getWaveform(SD: Track, forceUpdate: boolean): Promise<any>`
- `analyzeWaveform(tracks: Tracklist): void`

## Video and Stream Helpers

- `getVideoSettings(): any`
- `setVideoSettings(val: any): void`
- `repaintVideo(): void`
- `prepareAudioStreams(): StringList`
- `setAudioStream(strIdx: integer): void`
- `prepareSubtitlesAsync(): Promise<any>`
- `updateSubtitlesLanguage(langIdx: integer, fname: string): boolean`
- `updateSubtitlesSettings(): boolean`

## Miscellaneous Methods

- `refreshStatus(): void`
- `getDefaultAction(): string` (`playNow`, `playNext`, `playLast`, `properties`)
- `doOnLayoutModified(params: any): void`
- `isSoundTouchAccessible(): boolean`
- `assignUpdatedSonglist(newList: PlaylistEntries): void`
- `sendPlaybackCommand(command: integer, Args: any[]): void`

## Practical Patterns

Reliable toggle:

```js
if (app.player.isPlaying && !app.player.paused) {
	await app.player.pauseAsync();
} else {
	await app.player.playAsync();
}
```

Set current playing track by index and start:

```js
await app.player.setPlaylistPosAsync(index);
await app.player.playAsync();
```

Adjust volume safely:

```js
app.player.volume = Math.max(0, Math.min(1, app.player.volume + 0.05));
```

AutoDJ source extension pattern:

- Define custom source under `autoDJ.sources.<uniqueName>`
- Persist source config with `app.getValue` and `app.setValue`
- Return tracks from `execute(addCount)` via `autoDJ.getRandomList(...)`

```js
autoDJ.sources.myCustomSource = {
	name: () => 'Played# > X (autoDJ script)',
	order: 50,
	config: { playLimit: 10 },
	execute(addCount) {
		this.config = app.getValue('sampleAutoDJ_Addon', this.config);
		const tracks = app.db.getTracklist(
			'SELECT * FROM Songs WHERE PlayCounter > ' + this.config.playLimit,
			-1
		);
		return autoDJ.getRandomList(tracks, addCount);
	}
};
```

Replace Now Playing with a selected album and start immediately:

```js
await app.player.addTracksAsync(tracks, {
	withClear: true,
	startPlayback: true
});
```

Handle remote "play" commands defensively:

```js
if (app.player.entriesCount > 0) {
	r.asyncResult = true;
	await app.player.playAsync();
	const track = app.player.getCurrentTrack();
	r.responseBody = track
		? `Playback of track ${track.summary} started`
		: 'Playback started';
} else {
	r.responseBody = "No track in 'Playing' list";
}
```

Visualization lifecycle pattern:

```js
app.player.visualization.registerVisTypes(['vumeter']);
// ... use visualization data
app.player.visualization.unregisterVisTypes(['vumeter']);
```

Reuse buffers in visualization loops to reduce allocations:

```js
let oscCache = null;
let fftCache = null;

function onUpdate(visData) {
	oscCache = visData.getNormalizedOscValues(oscCache);
	fftCache = visData.getNormalizedFFTValues(fftCache);
}
```

Some engines treat returned arrays as either mono (512 values) or stereo-interleaved halves (1024 values split to L/R), so avoid hard-coding one fixed length in visualizers.

For animation/update loops, gate heavy work by playback state and content type (for example, skip visual animation for video tracks).

Update throttling is recommended, and frame updates should run only while current state is actively playing to avoid idle UI work when paused/stopped.

## Practical Checklist for Visualization Addons

Before shipping a visualization addon:

- Register only data types you actually need
- Unregister all types in `cleanUp()`
- Reuse result buffers when calling `VisResult` getters
- Handle resize changes idempotently
- Clear timers and state in `onStopHandler()`
- Guard for possible data-length variants when splitting channels
- Keep per-frame code lightweight (avoid heavy allocations in `onUpdateHandler`)

## API Cross-Check Notes

Cross-checked reference points:

- `Player` is documented as the class accessed through global `app.player`
- `addTracksAsync(tracks, params?)` supports the listed option semantics (`withClear`, `afterCurrent`, `startPlayback`, `shuffle`, `mix`, `byAlbum`, `position`, `saveHistory`, `forcePreview`, `focusedTrackIndex`)
- Visualization helpers are available (`getNormalizedFFTValues`, `getNormalizedOscValues`, `getNextResult`, `registerVisTypes`, `unregisterVisTypes`)
- Player event fields can appear in typings as `event_*` names, while `app.listen` uses plain event names like `playbackState` and `htmlPlaybackState`

## Notes for AI/Addon Authors

- Prefer async methods (`*Async`) for playback/list-changing operations
- `getSongList()` returns `PlaylistEntries`; use `getTracklist()` if you need `Tracklist` APIs
- For high-frequency UI refreshes, prefer `getFastCurrentTrack(existingObj)` over repeated fresh object creation
- Register only visualization types you really need, and call `unregisterVisTypes` when done
- Event names for `app.listen` are plain names like `playbackState` (not `event_playbackState`)


# Track Metadata and Database Reference

## 1. Object and DB Entry Points

### 1.1 Core objects at a glance

Track (`Native.Track`) common metadata fields:

- `title`
- `artist`
- `albumArtist`
- `album`
- `genre`
- `rating`
- `trackNumber`
- `year`
- `custom1` ... `custom10`

Track useful methods:

- `commitAsync(params?)`
- `getAlbumAsync()`
- `getCommentAsync()` / `setCommentAsync(value)`
- `getLyricsAsync()` / `setLyricsAsync(value)`
- `getExtendedTagsAsync()` / `setExtendedTagsAsync(value)`

Tracklist (`Native.Tracklist`) behavior:

- May be returned before fully loaded
- Always await `whenLoaded()` before using `count` or iterating
- Common APIs: `forEach`, `getValue(index)`, `count`, `getSelectedList()`
- Use `commitAsync(...)` to persist edits made to tracks in the list

Album (`Native.Album`) common fields:

- `title`
- `albumArtist`
- `description`

Album key method:

- `getTracklist(...params): Tracklist`

Artist (`Native.Artist`) common fields:

- `name`
- `title`
- `comment`
- `mbgid`

Artist commit methods:

- `commit()`
- `commitAsync()`

### 1.2 Database entry point

Use the global DB object:

```js
app.db
```

Main low-level SQL methods:

- `app.db.getQueryResultAsync(sql)` -> `Promise<QueryResults>` for `SELECT`
- `app.db.executeQueryAsync(sql)` -> `Promise<string>` for update/delete/DDL-like execution
- `app.db.insertQueryAsync(sql)` -> `Promise<number>` for insert, returns new record ID

High-level SQL-to-list methods:

- `app.db.getTracklist(sql, collID)` -> `Tracklist`
- `app.db.getAlbumList(sql, idColl?)` -> `AlbumList`
- `app.db.getArtistList(sql, idColl?)` -> `ArtistList`

## 2. Retrieving Tracklists and Objects

### 2.1 Selected tracks in UI

```js
const list = await uitools.getSelectedTracklist().whenLoaded();
if (list.count === 0) return;
```

`getSelectedTracklist()` can return `undefined` in some contexts, so guard in production code.

### 2.2 Current data source tracklist

```js
const list = uitools.getTracklist();
if (list) {
  await list.whenLoaded();
}
```

### 2.3 SQL-based tracklist

```js
const list = app.db.getTracklist(
  'SELECT * FROM Songs WHERE PlayCounter > 10',
  -1
);
await list.whenLoaded();
```

`collID` / collection argument:

- `-1` is commonly used for all collections

### 2.4 Album to tracklist

```js
const album = await app.getObject('album', { id: albumId });
if (album) {
  const albumTracks = await album.getTracklist().whenLoaded();
}
```

### 2.5 Resolve album from track

```js
const album = await track.getAlbumAsync();
if (album) {
  const albumTracks = await album.getTracklist().whenLoaded();
  console.log(album.title, albumTracks.count);
}
```

Alternative:

```js
const album = await app.getObject('album', { id: track.idalbum });
```

### 2.6 Player now-playing tracklist

```js
const nowPlaying = app.player.getTracklist();
await nowPlaying.whenLoaded();
```

### 2.7 Album and artist lists from SQL

```js
const albums = app.db.getAlbumList('SELECT * FROM Albums', -1);
const artists = app.db.getArtistList('SELECT * FROM Artists', -1);
```

## 3. Working with Tracklists and Metadata

### 3.1 Iterate and edit metadata

```js
const list = await uitools.getSelectedTracklist().whenLoaded();
if (list.count === 0) return;

let n = 1;
list.forEach((track) => {
  track.trackNumberInt = n;
  n++;
});

await list.commitAsync();
```

### 3.2 Batch edits and commit once

```js
const list = await uitools.getSelectedTracklist().whenLoaded();

list.forEach((t) => {
  t.artist = t.artist.trim();
  t.album = t.album.trim();
  t.title = t.title.trim();
});

await list.commitAsync();
```

This is usually better than calling `track.commitAsync()` in a tight loop.

### 3.3 Read long/lazy fields safely

```js
const fullComment = await track.getCommentAsync();
const fullLyrics = await track.getLyricsAsync();
const extTagsJson = await track.getExtendedTagsAsync();
```

Use corresponding async setters, then commit.

### 3.4 Tracklist performance notes

- `getFastObject(index, obj)` is fast but reuses objects; do not keep long-term references
- `getAllValues('fieldName')` is efficient for one field across many tracks
- Keep edits batched, then commit

## 4. Album/Artist Update Workflows

### 4.1 Album-level rename patterns

Two reliable approaches:

- Update per-track fields (`track.album`, `track.albumArtist`) and commit the `Tracklist`
- Run SQL for global album-name updates

SQL example:

```js
await app.db.executeQueryAsync(
  "UPDATE Albums SET Album = 'New Album Name' WHERE Albums.Album = 'Old Album Name'"
);
```

### 4.2 Artist updates via object API

```js
const artist = await app.getObject('artist', { id: artistId });
if (artist) {
  artist.name = 'Updated Name';
  await artist.commitAsync();
}
```

### 4.3 DB-level mass rename example

```js
await app.db.executeQueryAsync(
  "UPDATE Artists SET Artist = 'New Name' WHERE Artists.Artist = 'Old Name'"
);
```

Use with care because SQL updates can affect many rows beyond current selection.

## 5. SQL Query Workflows

### 5.1 Running SELECT queries

Basic single-value pattern:

```js
const sql = 'SELECT Count(*) FROM Songs';
const res = await app.db.getQueryResultAsync(sql);

if (!res.eof) {
  const count = parseInt(res.fields.getValue(0), 10);
  console.log('Track count:', count);
}
```

Multi-row iteration pattern:

```js
const res = await app.db.getQueryResultAsync('SELECT ID, SongTitle FROM Songs');

while (!res.eof) {
  const id = res.fields.getValue(0);
  const title = res.fields.getValue(1);
  // process row
  res.next();
}
```

`QueryResults` APIs used most:

- `res.eof`
- `res.fields.getValue(index)`
- `res.fieldByName(name)`
- `res.next()` / `res.prev()`
- `res.count` and `res.row`
- `res.names`

### 5.2 Running UPDATE/DELETE/DDL

Use `executeQueryAsync` for statements without row sets.

```js
await app.db.executeQueryAsync(
  "UPDATE Artists SET Artist = 'New Name' WHERE Artists.Artist = 'Old Name'"
);
```

If DB data is already cached, clear cache and refresh affected UI views:

```js
await app.db.executeQueryAsync("UPDATE Songs SET SongTitle = 'BlahBlah' WHERE ID = 1");
await app.db.clearCache();
uitools.refreshView();
```

### 5.3 Running INSERT and reading new ID

```js
const newId = await app.db.insertQueryAsync(
  "INSERT INTO MyTable(Name, Value) VALUES('x', 'y')"
);
console.log('Inserted row ID:', newId);
```

`insertQueryAsync` returns `Promise<number>`.

### 5.4 From SQL result to playback

```js
const res = await app.db.getQueryResultAsync(
  'SELECT id FROM albums ORDER BY RANDOM() LIMIT 1'
);

if (!res.eof) {
  const albumId = parseInt(res.fields.getValue(0), 10);
  const album = await app.getObject('album', { id: albumId });

  if (album) {
    const tracks = await album.getTracklist().whenLoaded();
    await app.player.addTracksAsync(tracks, {
      withClear: true,
      startPlayback: true
    });
  }
}
```

### 5.5 QueryData-based filtering APIs

Alternative filtering APIs:

- `app.db.getQueryData(...)`
- `app.db.trackSatisfyQueryData(...)`

Use these when MM query/filter semantics are preferable to manual SQL.

## 6. SQL Safety, Transactions, and Cache Refresh

### 6.1 Safe SQL construction

MM DB APIs here are SQL-string based. Escape dynamic values.

```js
function sqlQuote(str) {
  return "'" + String(str).replace(/'/g, "''") + "'";
}

const sql =
  'UPDATE Albums SET Album = ' + sqlQuote(newAlbum) +
  ' WHERE Albums.Album = ' + sqlQuote(oldAlbum);

await app.db.executeQueryAsync(sql);
```

Recommendations:

- Never concatenate raw user input
- Always quote/escape text values
- Parse numeric values before interpolation

### 6.2 Transactions for multi-step writes

DB transaction methods:

- `beginTransaction()`
- `commitTransaction()`
- `rollbackTransaction()`

Pattern:

```js
await app.db.beginTransaction();
try {
  await app.db.executeQueryAsync('UPDATE Songs SET Rating = 80 WHERE ID = 1');
  await app.db.executeQueryAsync('UPDATE Songs SET Rating = 60 WHERE ID = 2');
  await app.db.commitTransaction();
} catch (e) {
  await app.db.rollbackTransaction();
  throw e;
}
```

After transaction writes affecting visible data, clear cache and refresh views as needed.

## 7. Track Properties Dialog Usage and Extension

### 7.1 Typical usage flow

For user-facing metadata edit flows, addons often:

- Collect selected tracks with `uitools.getSelectedTracklist()`
- Open a custom dialog for pre-processing
- Write fields on tracks and commit

Optional help context:

- `helpContext: 'Editing Track Properties#Edit Tags'`

### 7.2 Add a custom Track Properties tab

Required files:

- `dialogs/dlgTrackProperties_add.js`
- `dialogs/dlgTrackProperties/tabMyCustom.js`
- `dialogs/dlgTrackProperties/tabMyCustom.html`

Register tab:

```js
propertiesTabs.tabMyCustom = {
  name: 'My custom tab',
  order: 35
};
```

Typical tab methods:

- `propertiesTabs.tabMyCustom.load(track, dialog)`
- `propertiesTabs.tabMyCustom.saveAsync(track, dialog)`
- Optional `propertiesTabs.tabMyCustom.updateVisibility(trackType, dialog)`

Minimal skeleton:

```js
propertiesTabs.tabMyCustom.load = function (track, dialog) {
  // initialize controls from track
};

propertiesTabs.tabMyCustom.saveAsync = function (track, dialog) {
  // write changes here, then return a promise
  return dummyPromise();
};

propertiesTabs.tabMyCustom.updateVisibility = function (trackType, dialog) {
  // optional per-type control visibility logic
};
```

Tab markup uses a regular dialog fragment rooted by `data-id="tabMyCustomContent"`.

### 7.3 Group edit and lifecycle behavior

- `dialog.isGroup` indicates multi-track edit mode
- Use `dialog.localPromise(...)` wrappers for async work tied to dialog lifecycle
- In group mode, avoid single-track assumptions

## 8. End-to-End Examples

### 8.1 Normalize selected track metadata

```js
async function normalizeSelectedTracks() {
  const list = uitools.getSelectedTracklist();
  if (!list) return;

  await list.whenLoaded();
  if (list.count === 0) return;

  list.forEach((t) => {
    t.artist = (t.artist || '').trim();
    t.albumArtist = (t.albumArtist || '').trim();
    t.album = (t.album || '').trim();
    t.title = (t.title || '').trim();
  });

  await list.commitAsync();
}
```

### 8.2 Count tracks

```js
const res = await app.db.getQueryResultAsync('SELECT Count(*) FROM Songs');
const count = !res.eof ? parseInt(res.fields.getValue(0), 10) : 0;
```

### 8.3 Get random tracklist and start playback

```js
const list = app.db.getTracklist('SELECT * FROM Songs ORDER BY RANDOM() LIMIT 50', -1);
await list.whenLoaded();
await app.player.addTracksAsync(list, { withClear: true, startPlayback: true });
```

### 8.4 Bulk update in transaction

```js
await app.db.beginTransaction();
try {
  await app.db.executeQueryAsync('UPDATE Songs SET SkipCount = 0 WHERE SkipCount > 0');
  await app.db.commitTransaction();
  await app.db.clearCache();
  uitools.refreshView();
} catch (e) {
  await app.db.rollbackTransaction();
  throw e;
}
```

## 9. Maintenance and Diagnostics APIs

Useful administrative methods:

- `getPath()`
- `getIsLocked()`
- `backupDatabase(fileName)` / `restoreDatabase(fileName)`
- `compactDatabase()`
- `rebuildDatabase()`
- `reCreateFullTextIndex()`

These are advanced operations; use cautiously in user-facing addons.

## 10. Pitfalls and Best Practices

- Always `await tracklist.whenLoaded()` before using `count`/iteration
- Prefer additive integration patterns (`*_add.js`) over full replacement where possible
- Batch metadata changes and commit once per list
- Use async accessors (`getCommentAsync`, `getLyricsAsync`, `getExtendedTagsAsync`) for full values
- Forgetting `res.next()` in SQL loops causes infinite loops
- Always check `res.eof` before assuming rows exist
- After DB updates, remember `clearCache()` + UI refresh when needed
- Escape SQL strings carefully
- Treat SQL-based updates as global operations
- In Track Properties custom tabs, handle both single-track and group edit modes
- Prefer `getTracklist` over `getQueryResultAsync` when the pipeline needs MM list objects for UI/playback

## 11. Quick API Cheat Sheet

Metadata and lists:

- `uitools.getSelectedTracklist()`
- `uitools.getTracklist()`
- `Tracklist.whenLoaded()`
- `Tracklist.forEach(...)`
- `Tracklist.commitAsync(...)`
- `app.getObject('album' | 'artist' | 'track', info)`
- `Album.getTracklist()`
- `Track.commitAsync(params?)`
- `Track.getCommentAsync() / setCommentAsync(...)`
- `Track.getLyricsAsync() / setLyricsAsync(...)`
- `Track.getExtendedTagsAsync() / setExtendedTagsAsync(...)`
- `propertiesTabs.<yourTab>.load/saveAsync/updateVisibility`

Database:

- `app.db.getQueryResultAsync(sql)`
- `app.db.executeQueryAsync(sql)`
- `app.db.insertQueryAsync(sql)`
- `app.db.getTracklist(sql, collID)`
- `app.db.getAlbumList(sql, idColl?)`
- `app.db.getArtistList(sql, idColl?)`
- `app.db.beginTransaction()` / `commitTransaction()` / `rollbackTransaction()`
- `app.db.clearCache()`

## 12. Where to Go Next

For related topics in this docs set:

- Player integration: `newDoc/APP_PLAYER_REFERENCE.md`
- Menu integration: `newDoc/MENU_EXTENSION_REFERENCE.md`
- Toolbar integration: `newDoc/TOOLBAR_ICON_REFERENCE.md`
- Combined menu/toolbar integration: `newDoc/MENU_AND_TOOLBAR_EXTENSION_REFERENCE.md`
- Tree-node data/view integration: `newDoc/TREE_NODE_CREATION_REFERENCE.md`
- Addon config and settings: `newDoc/ADDON_CONFIGURATION_REFERENCE.md`


# Addon Configuration Reference

## 1. Standard Addon Config Panel (Addons Dialog)

### 1.1 Register config entry in `info.json`

Set `config` in your addon manifest:

```json
{
  "id": "myAddon",
  "type": "general",
  "config": "config.js"
}
```

### 1.2 Implement `window.configInfo` in `config.js`

`config.js` must define `window.configInfo` with `load(panel, addon)` and `save(panel, addon)`.

```js
window.configInfo = {
  load: function (panel, addon) {
    this.state = app.getValue("myAddon_config", {
      enabled: true,
      mode: "basic"
    });

    const UI = getAllUIElements(panel);
    UI.chbEnabled.controlClass.checked = this.state.enabled;
  },

  save: function (panel, addon) {
    const UI = getAllUIElements(panel);
    this.state.enabled = UI.chbEnabled.controlClass.checked;
    app.setValue("myAddon_config", this.state);
  }
};
```

### 1.3 Optional matching `config.html`

You can add a matching HTML file with the same base name as config JS.

- `config.js` -> `config.html`
- `MyAddonConfig.js` -> `MyAddonConfig.html`

### 1.4 Behavior in Addons UI

When `config` is present:

- Addons UI shows a Configure button for the addon
- Config panel can open after install

## 2. Persisting Configuration (Save/Load)

## 2.1 Preferred store: `app.getValue` / `app.setValue`

API (`App`):

- `app.getValue(id, defaultValue): any`
- `app.setValue(id, value): void`
- `app.removeValue(id): void`
- `app.flushState(): void`

Pattern:

```js
const defaults = { hotkey: "Ctrl+Shift+R", enabled: true };
const cfg = app.getValue("myAddon_config", defaults);

cfg.enabled = false;
app.setValue("myAddon_config", cfg);
```

Best practices:

- Use addon-specific key prefixes (for example `myAddon_*`)
- Always pass defaults to `getValue`
- Store one object per logical feature area

## 2.2 INI-based storage (less common)

If needed, use INI bridge:

- `app.getIniFile(): IniFileAccess`
- `IniFileAccess.getStringValue/getIntValue/getBoolValue`
- `IniFileAccess.setStringValue/setIntValue/setBoolValue`
- `IniFileAccess.flush()` and `IniFileAccess.apply()`

Use this only when you specifically need INI semantics or compatibility.

## 3. Opening Addon Config Programmatically (`dlgAddonConfig`)

If you need to open another addon's config dialog from code:

```js
const list = app.getAddonList("all");
list.whenLoaded().then(() => {
  list.forEach((item) => {
    if (item.ext_id === "customNodes") {
      uitools.openDialog("dlgAddonConfig", {
        modal: true,
        addon: item
      });
    }
  });
});
```

Related API:

- `app.getAddonList(archiveType, ids?)`
- `openDialog(dialogName, params?, callback?)` (global)
- `app.dialogs.openDialog(name, params)`

Note:

- The `addon` object in config callbacks typically includes fields like `ext_id`, `title`, `description`, etc.

## 4. Add Controls/Panels to Main Options Dialog

There are two patterns in practice.

## 4.1 Add a subpanel under an existing Options section

Common and recommended pattern:

- Register panel in `dialogs/dlgOptions_add.js`
- Implement logic in `dialogs/dlgOptions/pnl_<yourPanelId>.js` (must be present even if empty!)
- Define UI in `dialogs/dlgOptions/pnl_<yourPanelId>.html` (must be present even if empty!)

Registration example:

```js
optionPanels.pnl_General.subPanels.pnl_myCustomSheet = {
  name: "My custom sheet"
  // order: 35
};
```

Load/save must be attached to the pnl_* object, for example:

```js
optionPanels.pnl_General.subPanels.pnl_myCustomSheet.load = function (sett) {
  this.state = app.getValue("_optionsPanelScript", { checkboxChecked: false });
  qid("chbMyCheckbox").controlClass.checked = this.state.checkboxChecked;
  var fmUI = getAllUIElements(qid('mySheetSettings'));
  setVisibility(fmUI.btnLogout, true);
  fmUI.chbScrobblerOn.controlClass.checked = false;
};

optionPanels.pnl_General.subPanels.pnl_myCustomSheet.save = function (sett) {
  this.state.checkboxChecked = qid("chbMyCheckbox").controlClass.checked;
  app.setValue("_optionsPanelScript", this.state);
};
```

You can find the panel using its ID by document.querySelector or similar. This can be useful it you want to access individual controls.

## 4.2 Add a top-level panel

Top-level registration pattern:

```js
optionPanels.pnl_myCustomSheet = {
  name: "My custom sheet"
};
```

Use a unique `pnl_*` id to avoid collisions with other addons.

## 4.3 Override existing built-in panel load/save (legacy/extending)

You can extend built-in panel behavior via override:

```js
optionPanels.pnl_General.override({
  load: function ($super, sett) {
    $super(sett);
    // custom logic
  },
  save: function ($super, sett) {
    $super(sett);
    // custom save
  }
});
```

## 5. Recommended Folder Layout

For an addon with both Addons-Dialog config and Options panel integration:

```text
myAddon/
  info.json                  // "config": "config.js"
  config.js                  // window.configInfo.load/save
  config.html                // optional Addons config UI
  dialogs/
    dlgOptions_add.js        // optionPanels registration
    dlgOptions/
      pnl_myAddon.js         // options panel load/save hooks
      pnl_myAddon.html       // options panel UI
```

## 6. Practical Checklist

- Add `"config": "config.js"` to `info.json` if you want Addons-dialog configuration
- Implement `window.configInfo.load/save`
- Persist settings via `app.getValue`/`app.setValue`
- If integrating with main Options UI, register in `dialogs/dlgOptions_add.js`
- Implement panel `load/save` hooks under `optionPanels...`
- Keep panel IDs and storage keys unique per addon
- Use `dlgAddonConfig` + `app.getAddonList("all")` when you need to open config programmatically


# UI Dialogs, Windows, and Controls Reference

## 1) Core UI Building Model

In MM addons, UI windows are usually HTML pages with a JS entry point:

- HTML file under addon path (often `dialogs/*.html` or `config.html`)
- JS file with `init(params)` function
- Controls declared via `data-control-class`
- Opened from addon code via `uitools.openDialog(...)` or `app.dialogs.openDialog(...)`

Typical dialog HTML shell:

```html
<html class="dialog">
<head>
  <script src="file:///mminit.js"></script>
  <script src="dlgMyDialog.js"></script>
</head>
<body data-posSaveName="dlgMyDialog" data-defaultSize="800,600">
  <!-- dialog content -->
</body>
</html>
```

## 2) Opening Dialogs and Windows

### 2.1 `uitools.openDialog(...)` (most common)

Signature:

```js
declare function openDialog(dialogName, params?, callback?): SharedWindow
```

Common usage:

```js
var dlg = uitools.openDialog('dlgCaseChecker', {
  show: true,
  modal: true,
  title: _('Case Checker'),
  tracks: list
});
```

Common `params` keys seen in practice:

- `modal`: open as modal dialog
- `show`: auto-show dialog (`true` by default in many flows)
- `title`: caption
- custom payload fields (e.g. `tracks`, `item`, `control`, `stateRestore`, etc.)
- `notShared`: optional separate instance behavior in some flows

### 2.2 `app.dialogs.openDialog(...)` (lower-level)

From typings:

```js
app.dialogs.openDialog(name, params): SharedWindow
```

This is used when more explicit window setup is needed, e.g. hidden/off-screen automation windows:

```js
w = app.dialogs.openDialog(url, {
  left: 0,
  top: 0,
  width: 1920,
  height: 1200,
  show: false,
  bordered: true,
  modal: false,
  canShow: false,
  openingWindow: thisWindow
});
```

### 2.3 Creating child windows directly: `createOwnWindow(...)`

Signature:

```js
declare function createOwnWindow(URL, params): SharedWindow
```

and method form on `SharedWindow`:

```js
createOwnWindow(URL, params): SharedWindow
```

Use this for child windows when you want explicit URL/geometry control.

### 2.4 Showing manually: `show()` and `showModal()`

Methods:

- `show(): void`
- `showModal(): void`

Pattern (open hidden first, show later):

```js
w.showModal();
```

## 3) Dialog Lifecycle

### 3.1 Entry point: `init(params)`

Dialog JS usually defines:

```js
function init(params) {
  var wnd = this;
  wnd.title = _('My Dialog');
  wnd.resizeable = true;
}
```

The incoming `params` object is what you pass via `openDialog(..., params)`.

### 3.2 Closing and result codes

Modal result is a core convention:

- `modalResult = 1` means accepted/OK in many MM dialogs
- `modalResult = 0` or unset means canceled/closed without commit

Example:

```js
window.localListen(btnOK, 'click', () => {
  // write back values
  modalResult = 1;
  closeWindow();
});
```

### 3.3 Listen for dialog close

Caller-side pattern:

```js
var dlg = uitools.openDialog('dlgEditCustomNode', { item, modal: true });
dlg.closed = function () {
  if (dlg.modalResult == 1) {
    // process accepted result
  }
};
app.listen(dlg, 'closed', dlg.closed);
```

You can also pass callback directly as 3rd `openDialog` arg for modal dialogs.

## 4) Passing Data In and Out

### 4.1 Input data (into dialog)

Pass plain values/objects in `params`:

```js
uitools.openDialog('dlgEditCustomNode', {
  item: newItem,
  modal: true
});
```

Dialog reads via `init(params)`.

### 4.2 Output data (from dialog)

There are two common patterns:

1. Mutate object passed in `params`:

```js
params.item.name = edtName.controlClass.value;
params.item.sql = edtSQL.controlClass.value;
modalResult = 1;
```

2. Expose a function on dialog window and fetch it from caller:

Dialog side:

```js
window.getHolds = function () { return holds; };
```

Caller side:

```js
var holds = dlg.getValue('getHolds')();
```

## 5) Building Dialog Content with Native Controls

### 5.1 Control declaration in HTML

Use `data-control-class` with optional `data-init-params` and `data-id`:

```html
<div data-id="edtName" data-control-class="Edit" class="fill"></div>
<div data-id="edtType" data-control-class="Dropdown" data-init-params="{readOnly: true, preload: true}"></div>
<div data-control-class="Buttons">
  <div data-id="btnOK" data-default="1"></div>
  <div data-id="btnCancel"></div>
</div>
```

### 5.2 Access controls in JS

- `getAllUIElements(...)` for bulk binding
- `qid('...')` for single element
- control instance via `.controlClass`

```js
let UI = getAllUIElements();
UI.edtName.controlClass.value = 'Example';
UI.btnOK.controlClass.disabled = false;
```

## 6) Common Native Controls (with practical usage)

All below are available native control classes.

### 6.1 `Buttons` / `Button`

HTML:

```html
<div data-control-class="Buttons">
  <div data-id="btnOK" data-default="1"></div>
  <div data-id="btnCancel"></div>
</div>
```

JS patterns:

```js
UI.btnOK.controlClass.disabled = true;
localListen(UI.btnOK, 'click', onOk);
```

Notes:

- Standard IDs include `btnOK`, `btnCancel`, `btnYes`, `btnNo`, etc. (`Buttons` class docs).

### 6.2 `Edit`

HTML:

```html
<div data-id="edtSQL" data-control-class="Edit" class="fill"></div>
```

JS:

```js
UI.edtSQL.controlClass.value = params.item.sql;
let sql = UI.edtSQL.controlClass.value;
```

Use for text or numeric entry (for numeric dialogs, many built-in dialogs pass `type: 'number'` in params).

### 6.3 `Dropdown`

HTML:

```html
<div data-id="edtType" data-control-class="Dropdown" data-init-params="{readOnly: true, preload: true}"></div>
```

JS:

```js
edtType.controlClass.addValue(_('Tracks'));
edtType.controlClass.addValue(_('Albums'));
edtType.controlClass.focusedIndex = 0;
```

Or bind a data source:

```js
UI.cbShowLinksTypes.controlClass.dataSource = ds;
UI.cbShowLinksTypes.controlClass.focusedIndex = fidx;
```

### 6.4 `Checkbox`

HTML:

```html
<div data-id="chbMyCheckbox" data-control-class="Checkbox">My Custom Checkbox</div>
```

JS:

```js
UI.chbMyCheckbox.controlClass.checked = true;
let isChecked = UI.chbMyCheckbox.controlClass.checked;
```

For radio behavior, use init params (`type: 'radio', name: 'GroupName'`) in HTML.

### 6.5 `Slider`

`Slider` is available in typings (`class Slider`), with properties like:

- `min`, `max`, `step`, `value`
- `orientation`
- `tickInterval`, `tickPlacement`, `ticks`

Example setup pattern:

```html
<div data-id="volumeSlider" data-control-class="Slider"></div>
```

```js
let s = qid('volumeSlider').controlClass;
s.min = 0;
s.max = 100;
s.step = 1;
s.value = 50;
```

Note: if your addon does not already include a slider dialog, use the class contract above as the baseline.

### 6.6 `GridView` (and tracklist-style controls)

Grid declaration:

```html
<div data-id="myGridView" data-control-class="GridView" class="fill"
     data-init-params="{isSortable: true, showHeader: true, multiselect: false, adaptColumnsWidth: false}"></div>
```

Minimal binding:

```js
UI.myGridView.controlClass.setColumns(columns);
UI.myGridView.controlClass.dataSource = myDataSource;
UI.myGridView.controlClass.fieldDefs = fieldDefs;
```

Also widely used:

- `TrackListView` derivatives (`CaseCheckerTracklist`)
- editor/list helpers like `ListEditButtons`, `SearchEditor`, `Scroller`

## 7) Dynamic/Embedded Control Composition

Not all controls are static HTML. Some dialogs set `data-control-class` at runtime and initialize:

```js
UI.customControl.setAttribute('data-control-class', params.control);
initializeControls(UI.parent);
```

Use this when one host dialog should render different control types depending on caller params.

## 8) Options Panels as UI (same control model)

Options sheets use the same control primitives and load/save lifecycle:

- register panel in `dlgOptions_add.js`
- implement `load` and `save` in panel JS

Example:

```js
optionPanels.pnl_General.subPanels.pnl_myCustomSheet.load = function () {
  qid('chbMyCheckbox').controlClass.checked = this.state.checkboxChecked;
};

optionPanels.pnl_General.subPanels.pnl_myCustomSheet.save = function () {
  this.state.checkboxChecked = qid('chbMyCheckbox').controlClass.checked;
  app.setValue('_optionsPanelScript', this.state);
};
```

## 9) Recommended Patterns

1. Prefer `uitools.openDialog(...)` unless you specifically need low-level `app.dialogs.openDialog(...)` behavior.
2. Pass all input through `params`; avoid global mutable state where possible.
3. Signal success with `modalResult = 1`; check it in caller before committing changes.
4. Keep returned data explicit: mutate input object or provide a named getter callable.
5. Use `getAllUIElements()` and `localListen(...)` for cleaner lifecycle and event cleanup.
6. Use `data-posSaveName` and `data-defaultSize` in dialog body for consistent window behavior.
7. For heavy list UIs, use `GridView`/Tracklist controls with data sources instead of manual DOM tables.

## 10) Copy-Paste Starter Templates

### 10.1 Basic modal dialog caller

```js
var dlg = uitools.openDialog('dlgMyDialog', {
  show: true,
  modal: true,
  title: _('My Dialog'),
  item: myItem
});

dlg.closed = function () {
  app.unlisten(dlg, 'closed', dlg.closed);
  if (dlg.modalResult !== 1)
    return;

  // Handle accepted result here
};
app.listen(dlg, 'closed', dlg.closed);
```

### 10.2 Dialog implementation skeleton

```js
function init(params) {
  var wnd = this;
  var UI = getAllUIElements();

  wnd.title = params.title || _('My Dialog');
  wnd.resizeable = true;

  UI.edtName.controlClass.value = params.item?.name || '';

  localListen(UI.btnOK, 'click', function () {
    if (params.item)
      params.item.name = UI.edtName.controlClass.value;

    modalResult = 1;
    closeWindow();
  });
}
```

### 10.3 GridView skeleton

```js
function init(params) {
  var UI = getAllUIElements();

  var columns = [
    {
      title: 'Name',
      width: 220,
      columnType: 'name',
      bindData: function (div, item) { div.innerText = item.name; }
    }
  ];

  var ds = new ArrayDataSource([{ name: 'Example' }]);

  UI.myGridView.controlClass.setColumns(columns);
  UI.myGridView.controlClass.dataSource = ds;
  UI.myGridView.controlClass.fieldDefs = {
    name: { title: 'Name', columnType: 'name', bindData: columns[0].bindData }
  };
}
```


# MediaMonkey Skin Creation, Editing, and Reuse Reference

## 1. Skin Basics

MediaMonkey skinning in this repository is structured as:

- `skin/`: Base skin (core LESS + base icons)
- `skins/<Skin Name>/`: Derived skin packages that extend/override the base skin

The base skin is compiled via `skin/skin_complete.less`, which imports the core LESS files in order.

### Base import order (`skin/skin_complete.less`)

The import order controls variable resolution and cascade behavior:

1. `skin_base.less`
2. `skin_runtime.less`
3. `skin_layout.less`
4. `skin_animations.less`
5. `skin_window_base.less`
6. `skin_scrollbar.less`
7. `skin_toolbar.less`
8. `skin_tabs.less`
9. `skin_listview.less`
10. `skin_treeview.less`
11. `skin_button.less`
12. `skin_rating.less`
13. `skin_icons.less`
14. `skin_dropdown.less`
15. `skin_progressbar.less`
16. `skin_progressline.less`
17. `skin_maskedit.less`
18. `skin_checkbox.less`
19. `skin_player.less`
20. `skin_controls.less`
21. `skin_menu.less`
22. `skin_slider.less`
23. `skin_search_editor.less`
24. `skin_equalizer.less`
25. `skin_albumtracklist.less`
26. `skin_episodelist.less`
27. `skin_statusbar.less`
28. `skin_compositebar.less`
29. `skin_mainwindow.less`
30. `skin_navigation.less`
31. `skin_toastmessage.less`
32. `skin_menupanel.less`
33. `skin_nowplaying.less`
34. `skin_graph.less`
35. `skin_custom.less`

Note: `skin_complete.less` imports `skin_runtime.less`, but `skin_runtime.less` is not stored in this repository tree. It is expected to be provided by the runtime/build environment.

## 2. How Derived Skins Work

Each derived skin lives under `skins/<Skin Name>/` and typically contains:

- `info.json`: Skin metadata (`"type": "skin"`, title, id, version, icon, optional `skin_options`)
- `thumb.png`: Preview/icon image
- `skin/`: LESS and asset overrides
- Optional extras such as `layouts/`, `controls/`, `templates_add.js`, `player.html`, `fonts/`, `skin/icon/`

### Override/add pattern used by MediaMonkey addons

The addon model supports:

- Full override by mirroring path/name (for example replacing a target file directly)
- Extension via `_add` suffix (for example `skin_base_add.less`)

In this repository's derived skins, the common and most important extension file is:

- `skin_base_add.less` (present in every derived skin)

Derived skins also frequently include additional section-specific add files:

- `skin_button_add.less`
- `skin_layout_add.less`
- `skin_mainwindow_add.less`
- `skin_menu_add.less`
- `skin_player_add.less`
- `skin_window_base_add.less`

Some skins fully replace specific files (for example `skin_scrollbar.less`) instead of only extending them.

## 3. Create a New Derived Skin

1. Create a folder under `skins/`, for example `skins/My Skin`.
2. Add `info.json` with at least:
   - `"type": "skin"`
   - `"title"`, `"id"`, `"version"`, `"author"`
   - `"icon": "thumb.png"`
3. Add `thumb.png` preview image.
4. Add `skin/skin_base_add.less` and override color variables, typography, spacing, and other base values.
5. Add additional `skin/*_add.less` files only where needed.
6. Add `skin/fonts/` and font-face declarations if custom fonts are required.
7. Add `skin/icon/` only for icon overrides/new icons.
8. Optionally include `layouts/`, `controls/`, `player.html`, or `templates_add.js` for UI/template-level changes.

## 4. Edit Existing Skins Safely

- Start from variables and tokens in `skin_base_add.less`.
- Prefer targeted section overrides (`skin_player_add.less`, `skin_menu_add.less`, etc.) rather than broad replacements.
- Replace full base files only when section-level add files are insufficient.
- Keep layout changes and color/theme changes separated where possible.
- Use SVG for icons; keep naming aligned with existing icon names unless intentionally adding new semantic icons.

## 5. Reuse Strategy (Recommended)

To maximize reuse and reduce maintenance cost:

- Put cross-cutting theme tokens in `skin_base_add.less`.
- Reuse existing base icon names and only override changed artwork.
- Create small, focused `_add.less` overrides by UI area.
- Keep custom fonts and icon assets local to each derived skin folder.
- Avoid cloning all base LESS files into a skin; only add what you need to change.

## 6. Base `skin/icon` Catalog

Below is the complete list of icon files currently available in `skin/icon`.

Notes:

- When used as a .js function call parameter or for 'data-icon' in html, use the icon name without extension (for example `music` for `music.svg`).

```text
about.svg
actor.svg
add.svg
addons.svg
addPlaylist.svg
addStorage.svg
album.svg
amazon.svg
analyzeVolume.svg
analyzeWaveform.svg
artist.svg
audiobook.svg
autoDJ.svg
autoplaylist.svg
autoTag.svg
blank.svg
browser.svg
by-nc.svg
by-sa.svg
cancel.svg
cast.svg
cd.svg
check.svg
checkVersion.svg
clearDatabase.svg
close.svg
cloud.svg
collapse.svg
collection.svg
columnlist.svg
composer.svg
computer.svg
conductor.svg
confirmation.svg
convert.svg
copy.svg
customize.svg
cut.svg
deadlinks.svg
delete.svg
device.svg
director.svg
downArrow.svg
downArrow2.svg
download.svg
drive.svg
dropdown.svg
edit.svg
eject.svg
email.svg
equalizer.svg
error.svg
exclsubfolders.svg
expand.svg
filter.svg
filter_remove.svg
folder.svg
folder_monitor.svg
folder_startup.svg
folders.svg
forum.svg
genre.svg
google.svg
gridview.svg
help.svg
home.svg
inclsubfolders.svg
information.svg
initialKey.svg
internet.svg
leftArrow.svg
levelVolume.svg
library.svg
link.svg
listview.svg
locate.svg
lyricist.svg
mainMenu.svg
maintainLib.svg
maximize.svg
mediamonkey.svg
menu.svg
menuArrow.svg
minimize.svg
mixedcontent.svg
mode_fullscreen.svg
mode_fullwindow.svg
mode_windowed.svg
mood.svg
move.svg
multipleArtists.svg
music.svg
nest.svg
network.svg
next.svg
none.svg
occasion.svg
openFile.svg
options.svg
organize.svg
party.svg
paste.svg
pause.svg
person.svg
pin.svg
play.svg
player_exit_full_screen.svg
player_next.svg
player_pause.svg
player_pause_bg.svg
player_play.svg
player_play_bg.svg
player_prev.svg
player_stop.svg
playLast.svg
playlist.svg
playMixed.svg
playNext.svg
playNow.svg
playpause.svg
podcast.svg
popupIndicator.svg
previous.svg
producer.svg
progress.svg
properties.svg
publisher.svg
quality.svg
radio.svg
redo.svg
refresh.svg
remove.svg
repeat.svg
repeatOne.svg
resize.svg
restore.svg
restoreFromMiniPlayer.svg
rightArrow.svg
rss.svg
save.svg
scan.svg
screenwriter.svg
search.svg
search_equals.svg
search_image.svg
search_library.svg
selectAll.svg
selectMode.svg
selectNone.svg
send.svg
series.svg
server.svg
showLess.svg
showMore.svg
showSidebarLeft.svg
showSidebarRight.svg
shuffle.svg
shuffleByAlbum.svg
side-left-view.svg
sleep.svg
song.svg
sort.svg
speed.svg
star.svg
stop.svg
support.svg
switchToMiniPlayer.svg
synchronize.svg
tabbed.svg
tag.svg
tempo.svg
trackBrowser.svg
treeCollapsed.svg
treeExpanded.svg
tv.svg
undo.svg
unknownAA.svg
upArrow.svg
upArrow2.svg
usb_connected.svg
video.svg
view.svg
visualization.svg
volume.svg
warning.svg
wikipedia.svg
windowclose.svg
year.svg
```

## 7. Practical Starting Point

If you are building a new skin from scratch in this repository, start with:

1. `info.json`
2. `thumb.png`
3. `skin/skin_base_add.less`
4. `skin/skin_icons_add.less` (only if icon styling needs changes)
5. Additional targeted `*_add.less` files as needed

This keeps the skin small, easier to maintain, and compatible with upstream base skin changes.


# Misc Useful API Patterns

## 1) Custom Local Sync Target Handler (`mediaSyncHandlers`)

This pattern registers a full custom sync backend:

```js
mediaSyncHandlers.syncToFolder = {
	syncEnabled(device) { return true; },
	initProfileDefaults(device) { ... },
	addCustomNodes(node) { ... },
	getStorageInfo(device) { ... },
	uploadTrack(sync, trackInfo, progress) { ... },
	uploadPlaylist(sync, playlist) { ... },
	scanContent(device, cancelToken) { ... },
	configPage: {
		summary: { load(device, config) { ... }, save(device, config) { ... } }
	}
};
```

Less obvious APIs used here:

- `mediaSyncDevices.register(...)` + `mediaSyncDevices.initRegistered()` for dynamic profile creation.
- `app.devices.getDeviceAsync(profileId, profileName)` to obtain the created profile instance.
- `device.getTargetMask(...)` / `device.setTargetMask(...)` to retarget sync masks after root changes.
- `device.getPlaylistSettingsJSON()` / `device.setPlaylistSettingsJSON(...)` for playlist export path updates.
- `device.resetTargetMasks()` to recompute target paths after sync-root edits.
- `device.setDriveLetter(letter)` in scan prep when a drive-like root is used.

## 2) Intercepting Core UI Behavior By Function Wrapping

This pattern wraps a global UI helper while preserving original behavior:

```js
if (!window.uitools._origShowPlaylistEditor)
	window.uitools._origShowPlaylistEditor = window.uitools.showPlaylistEditor;

window.uitools.showPlaylistEditor = function (plst, isNew, origin) {
	if (origin == 'sendToMenu' && !isNew) {
		uitools.toastMessage.show('Playlist edited in the background.', {
			disableUndo: true,
			disableClose: true,
			delay: 3000
		});
	} else {
		window.uitools._origShowPlaylistEditor.apply(this, arguments);
	}
};
```

This is a useful technique for selectively changing UX flow without rewriting entire controls.

## 3) Shared UI State Across Views Via Control Persistent State

This pattern hooks a control's persistent state methods to make column layout shared:

```js
ColumnTrackList.prototype.override({
	storePersistentState: function ($super, pState) {
		var state = $super(pState);
		app.setValue('sharedColumnsAddon_State', state);
		return state;
	},
	restorePersistentState: function ($super, state) {
		var sharedState = app.getValue('sharedColumnsAddon_State', state);
		$super(sharedState);
	}
});
```

Compared to generic config storage, this pattern is specifically about synchronizing component-level UI layout state between contexts.

## 4) Runtime Reconfiguration In Main Window From Config Dialog

Config dialog code can directly trigger main-window refresh logic:

```js
var win = app.dialogs.getMainWindow();
if (typeof win.getValue('ShowLinksSetup') == 'function')
	win.getValue('ShowLinksSetup')();
```

This avoids requiring app restart/reload after addon settings changes.

## 5) Extending Existing History System With New Public Action

This pattern introduces a new action surface (`window.actions.history.forward`) and connects it to history internals by adding methods to `NavigationHistory`:

```js
NavigationHistory.prototype.canRedo = function () { ... };
NavigationHistory.prototype.redo = function () { ... };
```

Then UI can consume these methods independently of player undo/redo APIs.

## 6) Lightweight Control Behavior Toggle By Prototype Override

Very small behavior customizations can be done with one override:

```js
MainTabs.prototype.override({
	initialize: function ($super, elem, params) {
		$super(elem, params);
		this.closeBtnOnHover = true;
	}
});
```

This is useful when the host control already has hidden feature flags and you only need to flip one.

## 7) Per-Field Tracklist Rendering Hook

Tracklist field definitions can be overridden with `bindData` to alter rendering per-row:

```js
uitools.tracklistFieldDefs.bitrate.override({
	bindData: function ($super, div, item) {
		$super(div, item);
		var bitrate = Math.round(item.bitrate / 1000);
		div.style.fontWeight = bitrate > 160 ? 'bold' : '';
		div.style.color = bitrate < 128 ? 'red' : '';
	}
});
```

This is an efficient extension point for visual indicators tied to metadata quality.

## 8) App-Level Remote Command Endpoint (`remoteRequest`)

You can listen for app-level remote HTTP requests and handle them asynchronously:

```js
app.listen(app, 'remoteRequest', (r) => {
	const content = JSON.parse(r.requestBody);
	if (content.command == 'play') {
		r.asyncResult = true;
		app.player.playAsync().then(() => {
			const track = app.player.getCurrentTrack();
			r.responseBody = track ? `Playback of ${track.summary} started` : 'Playback started';
		});
	}
});
```

Important implementation details:

- Parse JSON from `r.requestBody` and validate your own `target`/`clientID` contract.
- Set `r.asyncResult = true` before async work so response can be written later.
- Write status text to `r.responseBody` for caller feedback.

## 9) Programmatic Playlist Derivation Pattern

This pattern builds a derived playlist from a loaded source list:

```js
var list = pl.getTracklist();
var newList = app.utils.createTracklist();

list.whenLoaded().then(async () => {
	var hasher = {};
	listForEach(list, (track) => {
		if (!hasher[track.title]) newList.add(track);
		hasher[track.title] = true;
	});

	var newplaylist = app.playlists.root.newPlaylist();
	newplaylist.name = pl.name + ' (filtered)';
	await newplaylist.commitAsync();
	newplaylist.addTracksAsync(newList);
});
```

Useful API sequence:

- `playlist.getTracklist()` -> `whenLoaded()` before iteration.
- `app.utils.createTracklist()` as staging list.
- `app.playlists.root.newPlaylist()` -> set metadata -> `commitAsync()` -> `addTracksAsync(...)`.


# Related Doc

Cloud provider design and implementation details are at `cloud_providers_API.md`

How to use templates to modify how items (e.g. in album in listview) look like is described in `templates_API.md`

You can check out `mediamonkey.d.ts` to find the complete API with descriptions of methods.

Check out the `SampleScripts` folder for many examples of addons.