Appearance
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.jsonsetting.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 keysaddTo()— adds an entry to the settings navigationaddPage()— 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 usesaddToSetting(...). 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
}