Skip to content

SA Plugin Example — Music Player

A complete example of a Music Player plugin for the Standalone Application (SA) platform. This plugin adds a music widget and a settings page for managing playlists.

File Structure

music/
├── 1.0.0/
│   ├── setting.json
│   ├── script.js
│   ├── widget.vue
│   ├── setting.vue
│   ├── README-AR.md
│   ├── README-EN.md
│   └── images/
└── database/
    └── music/
        └── data.json

setting.json

The plugin manifest for an SA plugin. Note packages: [] for optional dependency declarations.

json
{
    "versionNumber": "0",
    "version": "1.0.0",
    "name": "music",
    "init": true,
    "type": "SA",
    "script": "script.js",
    "id": "_87847854244",
    "packages": []
}

script.js

The main registration script. SA uses a simplified API compared to EA — no roles system, and addTo() instead of addToSetting().

SA-specific APIs used:

  • addLanguage() — registers i18n translation keys
  • addTo() — adds an entry to the settings navigation
  • addPage() — registers a settings page with a short path (no /app/ prefix)
  • addWidget() — mounts a widget component onto the dashboard
js
plugins["music"] = function () {
    this.addLanguage("ar", "music", {
        name: "الموسيقى",
        playlist: "قائمة التشغيل"
    });
    this.addLanguage("en", "music", {
        name: "Music",
        playlist: "Playlist"
    });

    this.addTo({
        label: "Music",
        key: "music.name",
        icon: "mdi mdi-book-music",
        to: "/app/setting/music"
    }, "setting");

    this.addPage({
        name: "/[music]setting.vue",
        path: "music",
        file: ["music", "setting.vue"]
    }, "setting");

    this.addWidget({
        name: "/[music]widget.vue",
        id: "init-music",
        file: ["music", "widget.vue"]
    });
}

EA vs SA difference: SA uses addTo(..., "setting") while EA uses addToSetting(...). SA page paths are relative (e.g. "music"), EA paths are absolute (e.g. "/app/setting/music").

widget.vue

The dashboard music player widget. Identical in structure to the EA widget, but uses the api import instead of axios for data access.

vue
<template>
    <div id="box" class="col-12 lg:col-6 xl:col-6">
        <div class="audioPlayer" dir="ltr">
            <a class="nav-icon" @click="isPlaylistActive = !isPlaylistActive"
               :class="{ 'isActive': isPlaylistActive }">
                <span></span><span></span><span></span>
            </a>

            <div class="audioPlayerList" :class="{ 'isActive': isPlaylistActive }">
                <div class="item"
                     v-for="(item, index) in musicPlaylist"
                     :class="{ 'isActive': currentSong == index }"
                     @click="changeSong(index), isPlaylistActive = !isPlaylistActive"
                     :key="index">
                    <p class="title">{{ item.name }}</p>
                </div>
            </div>

            <div class="audioPlayerUI" :class="[{ 'isDisabled': isPlaylistActive }]">
                <div class="albumImage">
                    <transition name="fade" mode="out-in" appear>
                        <div :class="['disc-back', currentlyPlaying ? '' : 'paused']" :key="currentSong">
                            <img :src="img()" class="disc">
                            <img v-if="posterLoad" :src="musicPlaylist[currentSong].image" class="poster">
                        </div>
                    </transition>
                </div>

                <div class="albumDetails">
                    <transition name="slide-fade" mode="out-in" appear>
                        <p class="title" :key="currentSong">{{ musicPlaylist[currentSong]?.name }}</p>
                    </transition>
                    <div :class="['wave-container', currentlyPlaying ? '' : 'paused']">
                        <div v-for="index in 20" :key="index" class="wave-bar"></div>
                    </div>
                </div>

                <div class="playerButtons w-full">
                    <a class="button" @click="prevSong()">
                        <i class="mdi mdi-skip-previous icon text-4xl" />
                    </a>
                    <a class="button play" @click="playPauseAudio()">
                        <i :class="currentlyPlaying ? 'mdi mdi-pause' : 'mdi mdi-play'" class="icon text-4xl" />
                    </a>
                    <a class="button" @click="nextSong()">
                        <i class="icon mdi mdi-skip-next text-4xl" />
                    </a>
                </div>

                <div class="timeAndProgress">
                    <div class="currentTimeContainer">
                        <span class="currentTime">{{ currentTimeShow }}</span>
                        <span class="totalTime">{{ trackDurationShow }}</span>
                    </div>
                    <div class="currentProgressBar" ref="progress" @click="clickProgress">
                        <div class="currentProgress" :style="{ width: currentProgressBar + '%' }"></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import api from 'api'
import { join } from "node:path";

export default {
    // ... component logic
}
</script>

setting.vue

The settings page for playlist management. No role-based visibility (SA has no roles system).

vue
<template>
  <div>
    <div class="card flex align-items-center justify-content-between">
      <h5 class="m-0">{{ $t("music.name") }}</h5>
      <div>
        <Button icon="pi pi-save"    class="p-button-text" @click="save()" />
        <Button icon="pi pi-plus"    class="p-button-text" @click="products.playlist.push([])" />
        <Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
      </div>
    </div>

    <Accordion :activeIndex="0">
      <AccordionTab v-for="(item, index) in products.playlist" :key="index">
        <template #header>
          <RadioButton v-model="products.select" :value="index" />
          <span>{{ $t("music.playlist") }} {{ index + 1 }}</span>
        </template>

        <div>
          <Button icon="pi pi-plus"  class="p-button-text" @click="item.push({})" />
          <Button icon="pi pi-trash" class="p-button-text p-button-danger"
                  @click="products.playlist.splice(index, 1)" />
        </div>

        <DataTable :value="item" editMode="row" @row-edit-save="onRowEditSave($event, index)"
                   v-model:editingRows="editingRows">
          <Column field="name" :header="$t('form.name')" :sortable="true">
            <template #editor="{ data, field }">
              <InputText v-model="data[field]" class="w-full" />
            </template>
          </Column>
          <Column field="path" :header="$t('form.url')" :sortable="true">
            <template #editor="{ data, field }">
              <InputText v-model="data[field]" readonly class="w-full" />
            </template>
          </Column>
          <Column headerStyle="width:0px;" bodyStyle="text-align:end">
            <template #body="{ index }">
              <Button icon="pi pi-trash" class="p-button-text p-button-danger"
                      @click="item.splice(index, 1)" />
            </template>
          </Column>
          <Column :rowEditor="true" headerStyle="width: 110px;" bodyStyle="text-align:end" />
        </DataTable>
      </AccordionTab>
    </Accordion>
  </div>
</template>

database/music/data.json

Initial data structure, same shape across all platform types.

json
{
  "playlist": [],
  "select": 0
}