Add class registry (first pass)

In the first pass I add a `register` method and update `defineComponent`
so it calls `register` as well. This made it possible to create a
proof-of-concept for registered classes. Additionally ExportService was
added to expose registered classes to service scripts. This first pass
works, but it would be better if all types of classes (components or
otherwise) were registered via the same method.
This commit is contained in:
KernelDeimos 2024-05-27 21:14:10 -04:00
parent 146ce659e2
commit 51bac4486f
24 changed files with 171 additions and 41 deletions

View File

@ -161,6 +161,28 @@ class PuterHomepageService extends BaseService {
<!-- Preload images when applicable -->
<link rel="preload" as="image" href="${asset_dir}/images/wallpaper.webp">
<script>
if ( ! window.service_script ) {
window.service_script_api_promise = (() => {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
})();
window.service_script = async fn => {
try {
await fn(await window.service_script_api_promise);
} catch (e) {
console.error('service_script(ERROR)', e);
}
};
}
</script>
<!-- Files from JSON (may be empty) -->
${
((!bundled && manifest?.css_paths)

View File

@ -1,6 +1,8 @@
import { Component, defineComponent } from "../../util/Component.js";
export default class Button extends Component {
static ID = 'ui.component.Button';
static PROPERTIES = {
label: { value: 'Test Label' },
on_click: { value: null },
@ -65,4 +67,4 @@ export default class Button extends Component {
}
}
defineComponent('c-button', Button);
defineComponent(Button);

View File

@ -1,6 +1,8 @@
import { Component, defineComponent } from "../../util/Component.js";
export default class CodeEntryView extends Component {
static ID = 'ui.component.CodeEntryView';
static PROPERTIES = {
value: {},
error: {},
@ -215,4 +217,4 @@ export default class CodeEntryView extends Component {
}
}
defineComponent('c-code-entry-view', CodeEntryView);
defineComponent(CodeEntryView);

View File

@ -4,6 +4,8 @@ import { Component, defineComponent } from "../../util/Component.js";
* Display a list of checkboxes for the user to confirm.
*/
export default class ConfirmationsView extends Component {
static ID = 'ui.component.ConfirmationsView';
static PROPERTIES = {
confirmations: {
description: 'The list of confirmations to display',
@ -58,4 +60,4 @@ export default class ConfirmationsView extends Component {
}
}
defineComponent('c-confirmations-view', ConfirmationsView);
defineComponent(ConfirmationsView);

View File

@ -5,6 +5,8 @@ import { Component, defineComponent } from "../../util/Component.js";
* treated as a component.
*/
export default class Flexer extends Component {
static ID = 'ui.component.Flexer';
static PROPERTIES = {
children: {},
gap: { value: '20pt' },
@ -38,4 +40,4 @@ export default class Flexer extends Component {
}
}
defineComponent('c-flexer', Flexer);
defineComponent(Flexer);

View File

@ -4,6 +4,8 @@ import { Component, defineComponent } from "../../util/Component.js";
* Allows using an HTML string as a component.
*/
export default class JustHTML extends Component {
static ID = 'ui.component.JustHTML';
static PROPERTIES = { html: { value: '' } };
create_template ({ template }) {
$(template).html(`<span></span>`);
@ -15,4 +17,4 @@ export default class JustHTML extends Component {
}
}
defineComponent('c-just-html', JustHTML);
defineComponent(JustHTML);

View File

@ -1,6 +1,8 @@
import { Component, defineComponent } from "../../util/Component.js";
export default class PasswordEntry extends Component {
static ID = 'ui.component.PasswordEntry';
static PROPERTIES = {
spec: {},
value: {},
@ -133,4 +135,4 @@ export default class PasswordEntry extends Component {
}
}
defineComponent('c-password-entry', PasswordEntry);
defineComponent(PasswordEntry);

View File

@ -2,6 +2,8 @@ import { Component, defineComponent } from "../../util/Component.js";
import UIComponentWindow from "../UIComponentWindow.js";
export default class QRCodeView extends Component {
static ID = 'ui.component.QRCodeView';
static PROPERTIES = {
value: {
description: 'The text to encode in the QR code',
@ -78,4 +80,4 @@ export default class QRCodeView extends Component {
}
}
defineComponent('c-qr-code', QRCodeView);
defineComponent(QRCodeView);

View File

@ -1,6 +1,7 @@
import { Component, defineComponent } from "../../util/Component.js";
export default class RecoveryCodeEntryView extends Component {
static ID = 'ui.component.RecoveryCodeEntryView';
static PROPERTIES = {
value: {},
length: { value: 8 },
@ -84,4 +85,4 @@ export default class RecoveryCodeEntryView extends Component {
}
}
defineComponent('c-recovery-code-entry', RecoveryCodeEntryView);
defineComponent(RecoveryCodeEntryView);

View File

@ -1,6 +1,8 @@
import { Component, defineComponent } from "../../util/Component.js";
export default class RecoveryCodesView extends Component {
static ID = 'ui.component.RecoveryCodesView';
static PROPERTIES = {
values: {
description: 'The recovery codes to display',
@ -91,4 +93,4 @@ export default class RecoveryCodesView extends Component {
}
}
defineComponent('c-recovery-codes-view', RecoveryCodesView);
defineComponent(RecoveryCodesView);

View File

@ -22,6 +22,8 @@ import { Component, defineComponent } from "../../util/Component.js";
* Slider: A labeled slider input.
*/
export default class Slider extends Component {
static ID = 'ui.component.Slider';
static PROPERTIES = {
name: { value: null },
label: { value: null },
@ -111,4 +113,4 @@ export default class Slider extends Component {
}
}
defineComponent('c-slider', Slider);
defineComponent(Slider);

View File

@ -6,6 +6,8 @@ import { Component, defineComponent } from "../../util/Component.js";
* optimized for single-digit numbers.
*/
export default class StepHeading extends Component {
static ID = 'ui.component.StepHeading';
static PROPERTIES = {
symbol: {
description: 'The symbol to display',
@ -58,4 +60,4 @@ export default class StepHeading extends Component {
}
}
defineComponent('c-step-heading', StepHeading);
defineComponent(StepHeading);

View File

@ -1,6 +1,8 @@
import { Component, defineComponent } from "../../util/Component.js";
export default class StepView extends Component {
static ID = 'ui.component.StepView';
static PROPERTIES = {
children: {},
done: { value: false },
@ -64,4 +66,4 @@ export default class StepView extends Component {
}
}
defineComponent('c-step-view', StepView);
defineComponent(StepView);

View File

@ -5,6 +5,8 @@ import { Component, defineComponent } from "../../util/Component.js";
* specified style.
*/
export default class StringView extends Component {
static ID = 'ui.component.StringView';
static PROPERTIES = {
text: { value: '' },
heading: { value: 0 },
@ -42,4 +44,4 @@ export default class StringView extends Component {
}
}
defineComponent('c-string-view', StringView);
defineComponent(StringView);

View File

@ -4,6 +4,8 @@ import { Component, defineComponent } from "../../util/Component.js";
* A table with a sticky header
*/
export default class Table extends Component {
static ID = 'ui.component.Table';
static PROPERTIES = {
headings: { value: [] },
scale: { value: '2pt' },
@ -80,4 +82,4 @@ export default class Table extends Component {
}
}
defineComponent('c-table', Table);
defineComponent(Table);

View File

@ -4,6 +4,8 @@ import { Component, defineComponent } from "../../util/Component.js";
* A simple component when you just need to test something.
*/
export default class TestView extends Component {
static ID = 'ui.component.TestView';
static CSS = `
div {
background-color: lightblue;
@ -19,4 +21,4 @@ export default class TestView extends Component {
}
}
defineComponent('c-test-view', TestView);
defineComponent(TestView);

View File

@ -49,6 +49,7 @@ const end_process = async (uuid, force) => {
};
class TaskManagerTable extends Component {
static ID = 'ui.component.TaskManagerTable';
static PROPERTIES = {
tasks: { value: [] },
};
@ -157,9 +158,11 @@ class TaskManagerTable extends Component {
return rows;
};
}
defineComponent('c-task-manager-table', TaskManagerTable);
defineComponent(TaskManagerTable);
class TaskManagerRow extends Component {
static ID = 'ui.component.TaskManagerRow';
static PROPERTIES = {
name: {},
uuid: {},
@ -291,7 +294,7 @@ class TaskManagerRow extends Component {
});
}
}
defineComponent('c-task-manager-row', TaskManagerRow);
defineComponent(TaskManagerRow);
const UIWindowTaskManager = async function UIWindowTaskManager () {
const svc_process = globalThis.services.get('process');

View File

@ -17,6 +17,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export class Service {
construct () {
if ( ! this._construct ) return;
return this._construct();
}
init (...a) {
if ( ! this._init ) return;
return this._init(...a)

View File

@ -16,23 +16,6 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
window.service_script_api_promise = (() => {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
})();
window.service_script = async fn => {
try {
await fn(await window.service_script_api_promise);
} catch (e) {
console.error('service_script(ERROR)', e);
}
};
window.puter_gui_enabled = true;
/**

View File

@ -40,7 +40,9 @@ import { ProcessService } from './services/ProcessService.js';
import { PROCESS_RUNNING } from './definitions.js';
import { LocaleService } from './services/LocaleService.js';
import { SettingsService } from './services/SettingsService.js';
import { ExportService } from './services/ExportService.js';
import UIComponentWindow from './UI/UIComponentWindow.js';
import Spinner from './UI/Components/Spinner.js';
const launch_services = async function () {
// === Services Data Structures ===
@ -54,6 +56,9 @@ const launch_services = async function () {
services_m_[name] = instance;
}
const svc_export = new ExportService();
svc_export.register('UIComponentWindow', UIComponentWindow);
// === Hooks for Service Scripts from Backend ===
const service_script_deferred = { services: [], on_ready: [] };
const service_script_api = {
@ -61,7 +66,9 @@ const launch_services = async function () {
on_ready: fn => service_script_deferred.on_ready.push(fn),
// Some files can't be imported by service scripts,
// so this hack makes that possible.
use: name => ({ UIWindow, UIComponentWindow })[name],
use: svc_export.get.bind(svc_export),
exp: svc_export.register.bind(svc_export),
// use: name => ({ UIWindow, UIComponentWindow })[name],
};
globalThis.service_script_api_promise.resolve(service_script_api);
@ -71,6 +78,7 @@ const launch_services = async function () {
register('process', new ProcessService());
register('locale', new LocaleService());
register('settings', new SettingsService());
register('export', svc_export);
// === Service-Script Services ===
for (const [name, script] of service_script_deferred.services) {
@ -78,7 +86,13 @@ const launch_services = async function () {
}
for (const [_, instance] of services_l_) {
await instance.init();
await instance.construct();
}
for (const [_, instance] of services_l_) {
await instance.init({
services: globalThis.services,
});
}
// === Service-Script Ready ===

View File

@ -0,0 +1,24 @@
import { Service } from "../definitions.js";
/**
* This service is responsible for exporting definitions to the
* service script SDK. This is the SDK that services provided by
* the backend will use.
*/
export class ExportService extends Service {
constructor () {
super();
this.exports_ = {};
}
register (name, definition) {
this.exports_[name] = definition;
}
get (name) {
if ( name ) {
return this.exports_[name];
}
return this.exports_;
}
}

View File

@ -1,4 +1,5 @@
import ValueHolder from "./ValueHolder.js";
import { register } from "./register.js";
export class Component extends HTMLElement {
#has_created_element = false;
@ -146,10 +147,39 @@ export class Component extends HTMLElement {
}
}
export const defineComponent = (name, component) => {
// TODO: This is necessary because files can be loaded from
// both `/src/UI` and `/UI` in the URL; we need to fix that
if ( ! customElements.get(name) ) {
customElements.define(name, component);
// TODO: move this somewhere more useful
function is_subclass(subclass, superclass) {
if (subclass === superclass) return true;
let proto = subclass.prototype;
while (proto) {
if (proto === superclass.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
export const defineComponent = (component) => {
// Web components need tags (despite that we never use the tags)
// because it was designed this way.
if ( is_subclass(component, HTMLElement) ) {
console.log('defining', component);
let name = component.ID;
name = 'c-' + name.split('.').pop().toLowerCase();
// TODO: This is necessary because files can be loaded from
// both `/src/UI` and `/UI` in the URL; we need to fix that
console.log('[maybe] defining', name, 'as', component);
if ( customElements.get(name) ) return;
console.log('[surely] defining', name, 'as', component);
customElements.define(name, component);
component.defined_as = name;
}
// Service scripts aren't able to import anything when the
// GUI code is bundled, so we need to use a custom export
// mechanism for them.
register(component);
};

View File

@ -1,3 +1,5 @@
import { register } from "./register.js";
export default class TeePromise {
static STATUS_PENDING = {};
static STATUS_RUNNING = {};
@ -41,3 +43,5 @@ export default class TeePromise {
return this.then(fn);
}
}
register(TeePromise, 'TeePromise');

17
src/util/register.js Normal file
View File

@ -0,0 +1,17 @@
/**
* register registers a class with things that need classes
* to be registered. When in doubt, register your class.
*
* More specifically this function is here to handle such
* situations as service scripts not being able to import
* classes when the frontend is bundled.
*
* @param {*} cls
* @param {*} opt_name
*/
export const register = (cls, opt_name) => {
(async () => {
const api = await globalThis.service_script_api_promise;
api.exp(opt_name || cls.ID.split('.').pop(), cls);
})()
};