Melodia
下载

插件格式(format 2)

插件包是扩展名为 .melodiaplugin 的 zip 文件,在「设置 → 插件」(桌面端)或「设置 → 扩展 → 插件」(Android)中导入。导入时会先显示插件需要的权限,确认后才安装并启用。

包结构

文件 说明 上限
plugin.json 清单,必需 256KB
main.js 脚本入口;声明了页面、音源、歌词源或事件时必需 2MB
ui/*.json 页面与布局描述,只允许一层 单个 256KB
assets/*.png/jpg/jpeg/webp 图片,只允许一层 单个 2MB
plugin.jar 原生插件的代码,仅桌面端 64MB
native/windows-x64/*.dll 原生插件的动态库,可选,仅桌面端 单个 64MB

整个包最多 64 个文件,解压后不超过 10MB,原生插件不超过 200MB。沙盒插件不能包含 plugin.jar 和动态库。白名单之外的文件会被忽略,并在导入时提示。

清单

{
  "format": 1,
  "id": "com.example.lastfm",
  "name": "Last.fm 记录",
  "version": "1.0.0",
  "author": "someone",
  "description": "把听过的歌记录到 Last.fm",
  "minAppVersion": "1.6.0",
  "permissions": ["network", "playback.read", "storage"],
  "network": { "hosts": ["ws.audioscrobbler.com"] },
  "contributes": {
    "playerLayouts": { "desktop.playerBar": "ui/bar.json" },
    "pages": [{ "id": "stats", "title": "听歌统计", "icon": "chart", "ui": "ui/stats.json", "placement": "sidebar" }],
    "audioSources": [{ "id": "main", "name": "示例音源" }],
    "lyricsSources": [{ "id": "main", "name": "示例歌词" }],
    "events": ["songChanged", "playStateChanged"]
  }
}
字段 说明
format 必需,1 或 2;高于应用支持的版本时拒绝导入
id 必需;反向域名形式,只能用小写字母、数字和 _,如 com.example.demo。同 id 再次导入即升级
name / version 必需
author / description 可选
minAppVersion 可选,低于这个版本的应用不能安装;开发构建不校验
permissions 见下文;未知权限会被忽略并提示
network.hosts 申请 network 时必需;*.example.com 只匹配子域名,不匹配 example.com 本身
contributes 沙盒插件至少提供一项能力,否则插件无效;原生插件可以为空
kind format 2;sandbox(默认)或 native(桌面端原生插件,见「原生插件」一节)
main format 2;原生插件必需,入口类的完整类名
platforms format 2;可选,desktop、android,不写表示所有平台。不包含当前平台时拒绝导入,未知平台会被忽略并提示
apiVersion format 2;写成 1.0,沙盒插件可选,原生插件必需。主版本必须与应用一致,次版本不能高于应用,否则拒绝导入。当前应用提供 1.9

format 1 的插件继续有效,其中的 kind、platforms、apiVersion 会被忽略。

contributes

单项写错时只忽略这一项,并在导入时提示。

权限

权限 允许的操作 对应能力
network 访问 network.hosts 列出的域名
playback.read 读取当前歌曲、播放状态与进度,原生插件还可读取频谱 events 必需
playback.control 播放、暂停、切歌、加入队列
storage 插件私有的键值存储,上限 1MB
audioSource 作为音源提供播放链接 audioSources 必需
lyricsSource 作为歌词源提供歌词 lyricsSources 必需
ui.notify 显示应用内提示,提示前会带上插件名称
ui.openUrl 用系统浏览器打开网页,每次打开前询问用户
library.read 读取当前用户的歌单与红心列表
contentSource 提供歌曲、歌单与搜索结果 contentSources 必需
netease.read 以当前用户身份调用网易只读方法
netease.write 以当前用户身份调用网易写方法

提供了某项能力却没有申请对应权限时,整个插件无效。插件升级后如果申请了新的权限,导入预览会标出新增的权限。native 权限由 kind 决定,沙盒插件申请时会被忽略。

脚本

main.js 在隔离的 JS 环境中运行:Android 上是无界面的 WebView,桌面端是 GraalJS。脚本不能访问文件系统、Cookie、账号信息与宿主类,只能通过全局对象 melodia 调用宿主能力。所有调用都返回 Promise。

await melodia.http.request({ url, method, headers, body })   // 返回 { status, headers, body }
await melodia.storage.get(key)
await melodia.storage.set(key, value)                        // value 为 null 等同于删除
await melodia.storage.remove(key)
await melodia.player.current()                               // { id, songId, title, artist, cover } 或 null
await melodia.player.state()                                 // { playing, positionMs, durationMs }
await melodia.player.play() / pause() / next() / previous()
await melodia.player.enqueue({ songId, title, artist, cover })
await melodia.ui.setState(pageId, patch)                     // 浅合并到页面数据
await melodia.ui.toast(text)                                 // 不超过 120 个字符
await melodia.ui.openUrl(url)                                // 只接受 https,需要 API 1.4
await melodia.settings.get(key)
await melodia.settings.all()
await melodia.netease.call(method, params)
await melodia.library.likedSongIds()
await melodia.library.playlists()
await melodia.content.open(source, id, title)
melodia.on('songChanged', handler)
melodia.register.audioSource('main', { resolve, search })
melodia.register.lyricsSource('main', { fetch })
melodia.register.action('refresh', handler)
melodia.register.command('share', handler)
melodia.register.contentSource('main', { search, playlist, resolve })

音源

melodia.register.audioSource('main', {
  resolve: async function(song) {
    // song: { songId, title, artists, album, durationMs, origin, quality }
    return 'https://...';            // 或 { url: 'https://...' }
  }
});

歌词源

melodia.register.lyricsSource('main', {
  fetch: async function(song) {
    // song: { songId, title, artists, album, durationMs, origin }
    return '[00:01.00]第一句';       // LRC 文本,或 { lrc: '...' } / { ttml: '...' }
  }
});

网易接口

插件拿不到 Cookie 和登录凭证,只能调用下表方法。未登录时调用会被拒绝,响应只包含表里列出的数据。

const page = await melodia.netease.call('search', { keyword: '晴天', type: 'song' });
await melodia.netease.call('song.like', { id: page.items[0].songId, like: true });
方法 权限 参数 返回
song.detail netease.read ids(最多 200 个) 歌曲数组
song.lyric netease.read id { timeMs, text, translation } 数组
search netease.read keyword,type(song、album、artist、playlist,默认 song),offset,limit(最多 100) { total, hasMore, items }
playlist.detail / album.detail netease.read id 歌单对象
artist.detail / artist.topSongs netease.read id 歌手对象 / 歌曲数组
user.playlists netease.read 歌单数组
user.likedSongs netease.read 歌曲 id 数组
song.like netease.write id,like
playlist.create netease.write name,privacy(0 公开,10 隐私) 歌单对象
playlist.addTracks / playlist.removeTracks netease.write id,trackIds(最多 200 个)
playlist.subscribe netease.write id,subscribe
album.subscribe netease.write id,subscribe
artist.follow netease.write id,follow
playlist.delete netease.write id

写方法的限制:

歌曲对象为 { songId, title, artist, artists, album, albumId, cover, durationMs }。

内容源

内容源让第三方平台的内容进入应用:搜索结果显示在搜索页的「自定义插件」标签,歌单在原生详情页打开,歌曲可以播放和加入队列。

melodia.register.contentSource('main', {
  search: async function(query) {            // { keyword, offset, limit },清单里 search 为 true 才调用
    return [{ id: '1', title: '歌名', artist: '歌手', album: '专辑', cover: 'https://...', durationMs: 200000 }];
  },
  playlist: async function(args) {           // { id }
    return { name: '歌单', cover: 'https://...', description: '简介', tracks: [/* 同上 */] };
  },
  resolve: async function(track) {           // { id, quality }
    return 'https://...';                   // 或 { url: 'https://...' }
  }
});

// 打开原生歌单详情页,通常放在命令或页面动作里
await melodia.content.open('main', 'playlist-1', '我的歌单');

打开网页

await melodia.ui.openUrl('https://www.last.fm/api/auth/?api_key=...&token=...');

命令

"contributes": {
  "commands": [{ "id": "scrobble", "title": "记录到 Last.fm" }],
  "menus": [{ "command": "scrobble", "location": "song" }]
}
melodia.register.command('scrobble', async function(song) {
  // song 只在 location 为 song 时有值:{ songId, title, artist, album, cover }
});

外部平台的歌曲没有 songId。命令 5 秒内没有返回或抛错时,应用会提示执行失败,并计入连续出错。

设置

设置项写在清单里,应用生成设置页,入口在插件列表的设置按钮。

"settings": [
  { "id": "enabled", "type": "toggle", "title": "启用记录", "default": true },
  { "id": "token", "type": "text", "title": "令牌" },
  { "id": "mode", "type": "select", "title": "模式", "options": [{ "value": "fast", "label": "快速" }, { "value": "safe" }] },
  { "id": "interval", "type": "number", "title": "间隔", "min": 1, "max": 60, "step": 1, "default": 30 }
]
type 说明 专属字段
toggle 开关
text 文本,最长 500 个字符
select 单选,最多 32 项 options,每项 value 必需,label 可选
number 滑块 min、max 必需,step 默认 1

播放事件

事件 数据
songChanged { id, songId, title, artist, cover },停止播放时为 null
playStateChanged { playing }
progress { positionMs, durationMs },播放中每秒一次

事件只通知插件,不等待处理结果,也不能改变播放行为。插件启动时不会补发当前状态,需要时调用 melodia.player.current() 与 melodia.player.state()。

播放器布局

布局是由容器和组件组成的树,写在 ui/*.json 里,或直接内联在清单的 playerLayouts 下。只提供布局的插件不需要 main.js,也不需要申请权限。

{
  "type": "row", "padding": [0, 16], "spacing": 12, "align": "center", "height": 96,
  "children": [
    { "type": "cover", "shape": "circle", "maxSize": 64 },
    { "type": "songInfo", "weight": 1 },
    { "type": "column", "weight": 2, "align": "center", "children": [
      { "type": "controls", "buttons": ["shuffle", "previous", "playPause", "next", "repeat"], "size": "large" },
      { "type": "progress", "showTime": false }
    ]},
    { "type": "actions", "buttons": ["queue", "volume"] }
  ]
}

外框与变体

API 1.5 起,区域的值也可以写成带 layout 字段的对象,用来调整宿主保留的部分,或按尺寸换一套布局。直接写布局树的旧写法等于只写了 layout。

"android.fullPlayer": {
  "frame": { "header": "none", "cards": false },
  "layout": { "type": "column", "children": [] },
  "variants": [
    { "when": { "orientation": "landscape" }, "layout": { "type": "row", "children": [] } }
  ]
}
区域 frame 字段
android.fullPlayer header:host(默认,保留顶部的收起、播放来源与更多)或 none(去掉,由布局自己摆 collapseButton 等组件);cards:false 时去掉下方的信息卡片,首屏就是整页
android.fullPlayerWide split:左栏宽度占比,0.3–0.7,默认 0.4;左栏内容最宽 420
android.miniPlayer background:host(默认,封面取色)、none,或颜色、渐变(见「颜色」);radius:一个数字,或 [左上, 右上, 右下, 左下],默认 8;shadow、border:外框阴影与描边,默认 true
desktop.playerBar background:写颜色或渐变时给播放栏铺底;host(默认)与 none 都不铺底(API 1.6)

header 为 none 而布局里没有一定看得见的 collapseButton 时,宿主会在左上角补一个收起按钮。放在 pager 里、写了 visible 条件、不透明度低于 0.5、宽高小于 16 或静止时被动效淡出、缩小的都不算看得见;autoHide 由宿主控制,照常算。必需组件按同样的规则判断,被这样隐藏的播放/暂停按钮或进度条不算数,整个布局无效。下拉收起与返回键始终可用。

区域

key 位置 必需 不可用的组件
desktop.playerBar 桌面底部播放栏 controls 的 playPause、progress miniLyric、lyricsCard、pager
desktop.nowPlaying 桌面「正在播放」面板的封面与标题区 无 miniLyric、lyricsCard、actions
android.fullPlayer Android 全屏播放页首屏 playPause
android.fullPlayerWide Android 宽屏播放页 playPause lyricsCard、lyricsView、collapseButton、moreButton、sourceLabel
android.miniPlayer Android 迷你播放器 playPause actions、lyricsCard、lyricsView、pager、collapseButton、moreButton、sourceLabel
android.lyrics Android 全屏歌词页(API 1.9) lyricsView lyricsCard、sourceLabel
desktop.lyricsView 桌面歌词页,全屏时即沉浸播放页(API 1.9) lyricsView miniLyric、lyricsCard、moreButton、sourceLabel

两个歌词页区域替换整页内容,下拉收起、返回键、页面背景与留边不变;布局里没有一定看得见的 collapseButton 时,宿主同样在左上角补一个。这里的 lyricsView 就是歌词页原有的歌词列表,collapseButton 关闭歌词页。android.lyrics 的 actions 另有 lyricsSettings(打开歌词设置,需写明);desktop.lyricsView 的 actions 为 lyricsSettings、fullscreen、share,没写 buttons 时为后两个。

桌面区域不支持 collapseButton、moreButton、sourceLabel、lyricsView;time 与 playMode 按钮 API 1.7 起可用;desktop.nowPlaying 的 pager 放在可滚动的面板里,高度按页面内容取。

desktop.playerBar 根节点的 height 即播放栏高度,范围 64–160,默认 80。android.miniPlayer 根节点的 height 范围 48–140,controls 只支持 previous、playPause、next;左右滑动切歌仍然可用,但封面和标题不会跟手移动。

android.fullPlayer 占满一屏,默认顶部保留收起和更多按钮,下方的信息卡片不变;放了 lyricsCard 时,卡片列表里不再重复显示歌词卡;关掉卡片后,点 miniLyric 打开全屏歌词。手机横屏仍使用这个区域,可以用 orientation: landscape 的变体单独排版。android.fullPlayerWide 只在平板侧栏铺开到全屏时出现,只替换左栏,右栏歌词与卡片不变。

通用属性

属性 说明
padding 一个数字为四边相同;[上下, 左右];[上, 右, 下, 左]。范围 0–64
spacing 容器内子节点间距,范围 0–64
align start、center、end
weight 在 row / column 里按比例分配剩余空间,范围 0.1–10
width / height 固定尺寸,范围 0–1000;也可以写 "45%",按父容器剩余空间的比例(API 1.5)
minWidth / maxWidth / minHeight / maxHeight 尺寸上下限,范围 0–1000(API 1.5)
arrange row / column 主轴排列:start、center、end、spaceBetween、spaceAround、spaceEvenly;后三种不使用 spacing(API 1.5);row 写了 start 以外的排列又没写宽度时自动占满宽度(API 1.8)
alignSelf 在 box 里的位置:topStart、top、topEnd、start、center、end、bottomStart、bottom、bottomEnd;在 row / column 里只取交叉轴方向(API 1.5)
offset [x, y],范围 -200–200,只移动绘制位置,不影响布局(API 1.5)
background 颜色,如 pane、cardSurface;API 1.7 起也可以写其他颜色值或渐变,见「颜色」
contentColor 节点里文字与图标的颜色,次级、三级文字按透明度递减;宿主组件都会跟着变(API 1.7)
opacity 0–1,整个节点的不透明度(API 1.7)
elevation 0–24,按 shape 投影的阴影(API 1.7)
border { "width": 1, "color": "outline" },宽度 0–8(API 1.7)
textAlign start、center、end,用于 text、time、miniLyric(API 1.7)
marquee true 时单行放不下的文字横向滚动,用于 text、miniLyric(API 1.9)
autoHide true 时随歌词页的「自动隐藏控件」淡出并停止接收点击,不改变布局;lyricsView 不能写(API 1.9)
class 引用 styles 里的样式(API 1.7)
shape / radius rect、rounded、circle;radius 为圆角半径,默认 8;API 1.5 起可以写 [左上, 右上, 右下, 左下]

节点

type 说明 专属参数
column / row / box 容器,box 为叠放 children
pager 分页容器,左右滑动翻页,每个子节点一页,最多 5 页,不能嵌套;分页器里的封面不再左右滑动切歌(API 1.5) children、indicator(none、dots、bar)、initialPage
cover 封面,点击切换「正在播放」 maxSize(24–1000)、aspect(0.25–4,仅 Android)、size
songTitle / artist / songInfo 标题、歌手、两者组合 lines(1–4)、size
likeButton 收藏到歌单;播客为订阅电台
miniLyric 单行歌词(Android;迷你播放器 API 1.6 起可用)
progress 进度条 showTime
time 播放时间文字(Android API 1.6,桌面 API 1.7) format:position(默认)、duration、remaining、both(如 0:12 / 3:45);style、size
controls 播放控制 buttons:shuffle、previous、playPause、next、repeat、playMode(单键按列表循环、单曲循环、随机切换;Android API 1.6,桌面 API 1.7);size;variant:default、plain(播放键也是无底色图标)、outlined(圆形描边),API 1.7,迷你播放器不支持
actions 操作按钮 buttons,桌面:desktopLyric、lyricsView、queue、device、volume、miniPlayer、fullscreen;Android:device、queue、share、quality、more,API 1.6 起另有 sleepTimer、comments、lyrics(打开全屏歌词)
lyricsCard 歌词卡(Android)
lyricsView 整页滚动歌词,跟随用户的歌词设置,点某行跳到那里;每个布局(含每条变体)只能有一个(Android 全屏页,API 1.6)
spacer 留白,配合 weight 或固定尺寸
text 固定文字,不超过 40 个字符 text、style(title、body、caption)、lines、size
collapseButton 收起按钮;侧栏模式下带全屏切换(Android 全屏页,API 1.5)
moreButton 更多(Android 全屏页,API 1.5)
sourceLabel 播放来源,点击打开来源歌单(Android 全屏页,API 1.5)

size 取 small、medium、large。没写 buttons 时使用该区域的全部按钮,但 API 1.6 新增的 playMode、sleepTimer、comments、lyrics 需要写明才会出现。

播放状态绑定

API 1.6 起,播放器区域里 text 的文字和任意节点的 visible 可以写成 $player.xxx:

字段 值
title / artist / album 字符串
source 播放来源名称,如「每日推荐」、歌单名
quality 音质标签,如「无损」;用户关掉音质显示或播客时为空
playing / liked / isPodcast / hasLyrics 布尔值
playMode listLoop、singleLoop、shuffle
{ "type": "text", "text": "$player.quality", "visible": "$player.quality" }

写了不存在的字段,或在播放器区域用 $state / $item 时,这一项会被忽略并提示。进度不开放绑定,用 time 组件显示。桌面 API 1.7 起也可以绑定。

颜色

API 1.7 起,background、contentColor、border 的颜色和渐变色都可以写:

写法 说明
色板项 如 pane、textPrimary、accent,跟随皮肤
cover.base / cover.accent 封面取色的底色与高亮色
"#RRGGBB" / "#AARRGGBB" 固定颜色,深浅色共用
{ "light": "#…", "dark": "#…" } 按深浅色分别取色

渐变写成 { "gradient": ["cover.base", "pane"], "angle": 90 },2–4 个颜色,线性;angle 为 0 时从左到右,90 时从上到下,默认 90。

样式

API 1.7 起,区域外壳可以写 styles,节点用 class 引用:

{
  "styles": {
    "muted": { "contentColor": "textTertiary", "opacity": 0.8 },
    "accent": { "extends": "muted", "contentColor": { "light": "#2E8B57", "dark": "#4DAF7C" } }
  },
  "layout": { "type": "time", "class": "accent" }
}

动效

API 1.8 起,任意节点可以写 animation,只改变绘制,不影响布局;旋转、缩放后可能超出父容器,需要自己留出空间。layout 与每条变体各自最多 16 个节点带动效。用户关闭「界面动画」后,动效停在静止状态。

预设写成 "animation": "rotateWhilePlaying" 或 { "preset": "rotateWhilePlaying", "period": 12 }:

预设 效果 参数
rotateWhilePlaying 播放时匀速旋转,暂停时停在当前角度 period:转一圈的秒数,2–60,默认 20
scaleOnPause 暂停时缩小,播放时弹回 scale:暂停时的比例,0.5–1,默认 0.85
pulse 播放时轻微呼吸缩放 period:秒,0.5–10,默认 2;scale:最大比例,1–1.2,默认 1.04
songChange 切歌时进入 effect:fade(默认)、slideUp、zoom

关键帧:

"animation": {
  "keyframes": [{ "at": 0, "rotate": -25 }, { "at": 1, "rotate": 0 }],
  "duration": 600, "easing": "easeOut", "repeat": "once", "trigger": "playing"
}
字段 说明
keyframes 2–8 帧,at 为 0–1 且递增;每帧可写 scale(0–3)、rotate(度)、alpha(0–1)、x / y(-200–200),没写的属性沿用上一帧
duration 毫秒,100–60000,默认 1000
easing linear、ease(默认)、easeIn、easeOut
repeat once(默认)、loop、reverse(往返)
trigger always(默认);playing:开始播放时正放,暂停时倒放回第一帧;songChange:每次切歌播放一次;tap:点击这个节点时播放一次

songChange 与 tap 在触发前停在最后一帧。关闭界面动画时,只播一次的动效停在最后一帧,循环的停在第一帧,playing 按播放状态停在首帧或末帧。

手势

API 1.8 起,任意节点可以写 onTap、onDoubleTap、onLongPress,桌面上右键等同长按,右键不会触发 onTap。自带点击的组件(按钮、歌名等)先处理自己的点击,这时节点上的手势不触发;分页器的左右滑动、下拉收起与返回键不受影响。

动作 可用区域
togglePlay、next、previous、seekForward、seekBack(10 秒)、toggleLike、cyclePlayMode 全部
pagerNext、pagerPrevious 全部;让最近一层 pager 翻页,不在分页器里时无效
openLyrics、openQueue、openComments、openSleepTimer、openMore Android 全屏页、宽屏页;android.lyrics 可用除 openLyrics 外的四个
collapse Android 全屏页
toggleNowPlaying、toggleLyricsView 桌面播放栏、「正在播放」面板
closeLyrics、openLyricsSettings 两个歌词页区域(API 1.9)
script:<名称> 全部;调用 main.js 里 melodia.register.action 注册的同名动作,参数为当前播放状态(字段同 $player),5 秒内没有返回按失败处理

区域不支持的动作会被忽略并提示。沙盒插件用 script: 动作时,要在清单的 contributes 里写 "layoutActions": true 并带上 main.js,否则这些动作会被忽略;原生插件用 context.extensions.action 注册的动作同样可以调用,不需要声明 layoutActions。

校验与回退

页面

页面描述的写法与播放器布局相同,可用容器 column、row、box,以及 spacer、text 和下面的组件。提供页面的插件需要 main.js。

{
  "type": "column", "spacing": 12, "children": [
    { "type": "heading", "text": "$state.title" },
    { "type": "loading", "visible": "$state.loading" },
    { "type": "button", "text": "刷新", "action": "refresh" },
    { "type": "list", "bind": "$state.items", "spacing": 6, "children": [
      { "type": "row", "children": [
        { "type": "text", "text": "$item.name", "weight": 1 },
        { "type": "button", "text": "删除", "action": "remove" }
      ]}
    ]}
  ]
}
type 说明 必需字段
heading 标题 text
text 正文,可绑定 text
image 图片,bind 可写绑定或地址;只允许 assets/ 下的文件与 network.hosts 里的域名 bind
list 绑定数组,每一项按 children 渲染,最多 200 项;columns(1–8)大于 1 时按网格排列 bind
trackList 绑定 { songId, title, artist, cover } 数组,点击从这首开始播放整个列表;需要 playback.control bind
button 触发 action;在列表里时,当前项作为参数传给脚本,否则传点击时的页面数据(含输入框里还没提交的内容,API 1.4 起) text、action
toggle 开关,写回 $state 的第一层字段(input、select、slider 相同);有 action 时以 { value } 调用 text、bind
input 输入框,写回 $state 字段;有 action 时在回车后以 { value } 调用;secret 为 true 时遮挡显示(API 1.4 起) text(提示文字)、bind
loading / empty 加载中与空态
select 单选,写回 $state 字段;有 action 时以 { value } 调用 text(标题)、bind、options
slider 滑块,写回 $state 字段;松手后调用 action text(标题)、bind;min、max 默认 0、1,step 可选
progressBar 进度条,绑定 0–1 的数字 bind
badge 小标签 text
tabs 标签页,每个子节点是一页,子节点的 text 为标题 children

动态背景

替换窗口背景,优先于皮肤的背景图。目前只有桌面端生效。多个背景在「设置 → 插件 → 动态背景」里选择,默认用最近启用的插件,也可以选「不使用」。

"backdrops": [
  { "id": "rain", "name": "雨夜", "fps": 12, "dim": 0.4, "paneOpacity": 0.6, "frames": ["assets/rain1.png", "assets/rain2.png"] }
]
字段 说明
id / name id 必需,name 可选
fps 1–30,默认 12
dim 背景上的遮罩,0–1,默认 0.3
paneOpacity 面板不透明度,0–1,默认 0.6
frames assets/ 下的图片,循环播放,最多 32 帧,长边缩到 1280。沙盒插件必需

原生插件可以不写 frames,改用代码逐帧渲染:

context.extensions.backdropSource("aurora", object : BackdropSource {
    override val maxWidth = 320

    override fun render(frame: BackdropFrame) {
        // 把不透明的 ARGB 写进 frame.pixels,长度为 width * height
    }
})

render 在后台线程调用,帧率与遮罩来自清单,宿主把画面拉伸铺满。窗口隐藏或最小化时不调用;关闭界面动画时只渲染一帧。需要 API 1.1。沙盒脚本不能逐帧绘制。

分发

除了手动导入,还可以提交到插件目录,见 plugin-catalog.md。升级时保留上一版本,可以在插件列表里回退,插件数据不变。

原生插件

只有桌面端支持,用于沙盒做不到的场景。原生插件在主进程运行,等同于运行任意程序:权限声明只对通过 API 的调用生效,无法真正限制它。Android 导入时直接拒绝。

要先在「设置 → 插件」开启「允许原生插件」(默认关闭)才能导入和加载。

class HelloPlugin : MelodiaPlugin {
    override fun onLoad(context: PluginContext) {
        context.extensions.command("hello") {
            context.ui.toast("你好")
        }
    }
}

完整示例在 plugin-sample 模块,gradlew :plugin-sample:packagePlugin 生成可导入的包。

存储