mirror of
https://github.com/HeyPuter/puter.git
synced 2025-01-27 09:48:43 +08:00
151 lines
5.0 KiB
Markdown
151 lines
5.0 KiB
Markdown
|
> **NOTICE:** This documentation is new and might contain errors.
|
||
|
> Feel free to open a Github issue if you run into any problems.
|
||
|
|
||
|
# Service Scripts
|
||
|
|
||
|
## What is a Service Script?
|
||
|
|
||
|
Service scripts allow backend services to provide client-side code that
|
||
|
runs in Puter's GUI. This is useful if you want to make a mod or plugin
|
||
|
for Puter that has backend functionality. For example, you might want
|
||
|
to add a tab to the settings panel to make use of or configure the service.
|
||
|
|
||
|
Service scripts are made possible by the `puter-homepage` service, which
|
||
|
allows you to register URLs for additional javascript files Puter's
|
||
|
GUI should load.
|
||
|
|
||
|
## ES Modules - A Problem of Ordering
|
||
|
|
||
|
In browsers, script tags with `type=module` implicitly behave according
|
||
|
to those with the `defer` attribute. This means after the DOM is loaded
|
||
|
the scripts will run in the order in which they appear in the document.
|
||
|
|
||
|
Relying on this execution order however does not work. This is because
|
||
|
`import` is implicitly asynchronous. Effectively, this means these
|
||
|
scripts will execute in arbitrary order if they all have imports.
|
||
|
|
||
|
In a situation where all the client-side code is bundled with rollup
|
||
|
or webpack this is not an issue as you typically only have one
|
||
|
entry script. To facilitate loading service scripts, which are not
|
||
|
bundled with the GUI, we require that service scripts call the global
|
||
|
`service_script` function to access the API for service scripts.
|
||
|
|
||
|
## Providing a Service Script
|
||
|
|
||
|
For a service to provide a service script, it simply needs to serve
|
||
|
static files (the "service script") on some URL, and register that
|
||
|
URL with the `puter-homepage` service.
|
||
|
|
||
|
In this example below we use builtin functionality of express to serve
|
||
|
static files.
|
||
|
|
||
|
```javascript
|
||
|
class MyService extends BaseService {
|
||
|
async _init () {
|
||
|
// First we tell `puter-homepage` that we're going to be serving
|
||
|
// a javascript file which we want to be included when the GUI
|
||
|
// loads.
|
||
|
const svc_puterHomepage = this.services.get('puter-homepage');
|
||
|
svc_puterHomepage.register_script('/my-service-script/main.js');
|
||
|
}
|
||
|
|
||
|
async ['__on_install.routes'] (_, { app }) {
|
||
|
// Here we ask express to serve our script. This is made possible
|
||
|
// by WebServerService which provides the `app` object when it
|
||
|
// emits the 'install.routes` event.
|
||
|
app.use('/my-service-script',
|
||
|
express.static(
|
||
|
PathBuilder.add(__dirname).add('gui').build()
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## A Simple Service Script
|
||
|
|
||
|
|
||
|
|
||
|
```javascript
|
||
|
import SomeModule from "./SomeModule.js";
|
||
|
|
||
|
service_script(api => {
|
||
|
api.on_ready(() => {
|
||
|
// This callback is invoked when the GUI is ready
|
||
|
|
||
|
// We can use api.get() to import anything exposed to
|
||
|
// service scripts by Puter's GUI; for example:
|
||
|
const Button = api.use('ui.components.Button');
|
||
|
// ^ Here we get Puter's Button component, which is made
|
||
|
// available to service scripts.
|
||
|
});
|
||
|
});
|
||
|
```
|
||
|
|
||
|
## Adding a Settings Tab
|
||
|
|
||
|
Starting with the following example:
|
||
|
|
||
|
```javascript
|
||
|
import MySettingsTab from "./MySettingsTab.js";
|
||
|
|
||
|
globalThis.service_script(api => {
|
||
|
api.on_ready(() => {
|
||
|
const svc_settings = globalThis.services.get('settings');
|
||
|
svc_settings.register_tab(MySettingsTab(api));
|
||
|
});
|
||
|
});
|
||
|
```
|
||
|
|
||
|
The module **MySettingsTab** exports a function for scoping the `api`
|
||
|
object, and that function returns a settings tab. The settings tab is
|
||
|
an object with a specific format that Puter's settings window understands.
|
||
|
|
||
|
Here are the contents of `MySettingsTab.js`:
|
||
|
|
||
|
```javascript
|
||
|
import MyWindow from "./MyWindow.js";
|
||
|
|
||
|
export default api => ({
|
||
|
id: 'my-settings-tab',
|
||
|
title_i18n_key: 'My Settings Tab',
|
||
|
icon: 'shield.svg',
|
||
|
factory: () => {
|
||
|
const NotifCard = api.use('ui.component.NotifCard');
|
||
|
const ActionCard = api.use('ui.component.ActionCard');
|
||
|
const JustHTML = api.use('ui.component.JustHTML');
|
||
|
const Flexer = api.use('ui.component.Flexer');
|
||
|
const UIAlert = api.use('ui.window.UIAlert');
|
||
|
|
||
|
// The root component for our settings tab will be a "flexer",
|
||
|
// which by default displays its child components in a vertical
|
||
|
// layout.
|
||
|
const component = new Flexer({
|
||
|
children: [
|
||
|
// We can insert raw HTML as a component
|
||
|
new JustHTML({
|
||
|
no_shadow: true, // use CSS for settings window
|
||
|
html: '<h1>Some Heading</h1>',
|
||
|
}),
|
||
|
new NotifCard({
|
||
|
text: 'I am a card with some text',
|
||
|
style: 'settings-card-success',
|
||
|
}),
|
||
|
new ActionCard({
|
||
|
title: 'Open an Alert',
|
||
|
button_text: 'Click Me',
|
||
|
on_click: async () => {
|
||
|
// Here we open an example window
|
||
|
await UIAlert({
|
||
|
message: 'Hello, Puter!',
|
||
|
});
|
||
|
}
|
||
|
})
|
||
|
]
|
||
|
});
|
||
|
|
||
|
return component;
|
||
|
}
|
||
|
});
|
||
|
```
|