Mirror of https://github.com/improsocial/impro-sample-plugin
0

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

Grace Kind (May 20, 2026, 3:28 PM -0500) e006da7f

+1775
+30
.github/workflows/release.yml
··· 1 + name: Release 2 + 3 + on: 4 + push: 5 + tags: 6 + - "*" 7 + 8 + permissions: 9 + contents: write 10 + 11 + jobs: 12 + release: 13 + runs-on: ubuntu-latest 14 + steps: 15 + - uses: actions/checkout@v4 16 + 17 + - uses: actions/setup-node@v4 18 + with: 19 + node-version: "20" 20 + 21 + - run: npm ci 22 + # - run: npm run build 23 + 24 + - name: Create or update release 25 + env: 26 + GH_TOKEN: ${{ github.token }} 27 + run: | 28 + gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 \ 29 + || gh release create "$GITHUB_REF_NAME" --generate-notes 30 + gh release upload "$GITHUB_REF_NAME" manifest.json main.js --clobber
+2
.gitignore
··· 1 + node_modules 2 + .claude
+545
main.js
··· 1 + // src/pluginWorker.js 2 + var SimpleUUID = class { 3 + constructor() { 4 + this._id = 0; 5 + } 6 + create() { 7 + return this._id++; 8 + } 9 + }; 10 + var uuid = new SimpleUUID(); 11 + var callHandlers = /* @__PURE__ */ new Map(); 12 + var pendingHostCalls = /* @__PURE__ */ new Map(); 13 + function hostCall(method, ...args) { 14 + const hostCallId = uuid.create(); 15 + return new Promise((resolve, reject) => { 16 + pendingHostCalls.set(hostCallId, { resolve, reject }); 17 + self.postMessage({ type: "hostCall", method, hostCallId, args }); 18 + }); 19 + } 20 + var eventListeners = /* @__PURE__ */ new Map(); 21 + function addEventListener(event, listener) { 22 + let listeners = eventListeners.get(event); 23 + if (!listeners) { 24 + listeners = /* @__PURE__ */ new Set(); 25 + eventListeners.set(event, listeners); 26 + const handlerId = uuid.create(); 27 + callHandlers.set(handlerId, async (...args) => { 28 + const menu = new Menu(); 29 + for (const eventListener of listeners) { 30 + try { 31 + await eventListener(menu, ...args); 32 + } catch (error) { 33 + console.error(`"${event}" listener threw:`, error); 34 + } 35 + } 36 + return menu._serialize(); 37 + }); 38 + self.postMessage({ 39 + type: "register", 40 + target: "eventListener", 41 + event, 42 + handlerId 43 + }); 44 + } 45 + listeners.add(listener); 46 + } 47 + var MenuItem = class { 48 + constructor() { 49 + this.title = ""; 50 + this.icon = null; 51 + this._callback = () => { 52 + }; 53 + } 54 + setTitle(title) { 55 + this.title = title; 56 + return this; 57 + } 58 + setIcon(icon) { 59 + this.icon = icon; 60 + return this; 61 + } 62 + onClick(callback) { 63 + this._callback = callback; 64 + return this; 65 + } 66 + }; 67 + var Menu = class { 68 + constructor() { 69 + this.items = []; 70 + } 71 + addItem(builder) { 72 + const item = new MenuItem(); 73 + builder(item); 74 + this.items.push(item); 75 + return this; 76 + } 77 + _serialize() { 78 + return this.items.map((item) => { 79 + const handlerId = uuid.create(); 80 + callHandlers.set(handlerId, item._callback); 81 + return { title: item.title, icon: item.icon, handlerId }; 82 + }); 83 + } 84 + }; 85 + var App = class { 86 + constructor() { 87 + this.currentUser = null; 88 + } 89 + on(event, listener) { 90 + addEventListener(event, listener); 91 + } 92 + refreshFeedFilters(feedURI = null) { 93 + return hostCall("refreshFeedFilters", feedURI); 94 + } 95 + }; 96 + var registered = false; 97 + var Plugin = class { 98 + constructor() { 99 + this.app = new App(); 100 + } 101 + addSidebarItem(icon, title, callback = () => { 102 + }) { 103 + const handlerId = uuid.create(); 104 + callHandlers.set(handlerId, callback); 105 + self.postMessage({ 106 + type: "register", 107 + target: "sidebarItem", 108 + icon, 109 + title, 110 + handlerId 111 + }); 112 + } 113 + async loadData() { 114 + return hostCall("loadData"); 115 + } 116 + async saveData(data) { 117 + await hostCall("saveData", { data }); 118 + } 119 + addSettingTab(tab) { 120 + tab.plugin = this; 121 + const displayHandlerId = uuid.create(); 122 + callHandlers.set(displayHandlerId, () => { 123 + tab.containerEl = new VirtualEl("div"); 124 + tab.display(); 125 + return tab.containerEl._serialize(); 126 + }); 127 + self.postMessage({ 128 + type: "register", 129 + target: "settingTab", 130 + name: tab.name ?? null, 131 + displayHandlerId 132 + }); 133 + this._settingTab = tab; 134 + } 135 + addFeedFilter(callback = () => { 136 + }) { 137 + const handlerId = uuid.create(); 138 + callHandlers.set(handlerId, callback); 139 + self.postMessage({ 140 + type: "register", 141 + target: "feedFilter", 142 + handlerId 143 + }); 144 + } 145 + onload() { 146 + } 147 + onunload() { 148 + } 149 + static register() { 150 + if (registered) return; 151 + registered = true; 152 + const instance = new this(); 153 + hostCall("getCurrentUser").then((user) => { 154 + instance.app.currentUser = user; 155 + return instance.onload(); 156 + }).then( 157 + () => self.postMessage({ type: "ready" }), 158 + (error) => self.postMessage({ 159 + type: "ready", 160 + error: error?.message ?? String(error) 161 + }) 162 + ); 163 + } 164 + }; 165 + var openModals = /* @__PURE__ */ new Map(); 166 + var Modal = class { 167 + constructor() { 168 + this._modalId = uuid.create(); 169 + this.contentEl = new VirtualEl("div"); 170 + this.titleEl = new VirtualEl("h2"); 171 + } 172 + open() { 173 + if (openModals.has(this._modalId)) return; 174 + openModals.set(this._modalId, this); 175 + this.onOpen(); 176 + self.postMessage({ 177 + type: "hostCall", 178 + method: "openModal", 179 + args: [ 180 + { 181 + modalId: this._modalId, 182 + title: this.titleEl._serialize(), 183 + content: this.contentEl._serialize() 184 + } 185 + ] 186 + }); 187 + } 188 + close() { 189 + if (!openModals.has(this._modalId)) return; 190 + openModals.delete(this._modalId); 191 + self.postMessage({ 192 + type: "hostCall", 193 + method: "closeModal", 194 + args: [{ modalId: this._modalId }] 195 + }); 196 + this.onClose(); 197 + } 198 + onOpen() { 199 + } 200 + onClose() { 201 + } 202 + }; 203 + var PluginSettingTab = class { 204 + constructor() { 205 + this.containerEl = new VirtualEl("div"); 206 + this.name = null; 207 + } 208 + setName(name) { 209 + this.name = name; 210 + return this; 211 + } 212 + display() { 213 + } 214 + refresh() { 215 + return hostCall("refreshSettingTab"); 216 + } 217 + }; 218 + var Setting = class { 219 + constructor(containerEl) { 220 + this.settingEl = containerEl.createDiv({ cls: "plugin-setting-item" }); 221 + this.infoEl = this.settingEl.createDiv({ cls: "plugin-setting-item-info" }); 222 + this.nameEl = this.infoEl.createDiv({ cls: "plugin-setting-item-name" }); 223 + this.descEl = this.infoEl.createDiv({ cls: "plugin-setting-item-desc" }); 224 + this.controlEl = this.settingEl.createDiv({ 225 + cls: "plugin-setting-item-control" 226 + }); 227 + } 228 + setName(text) { 229 + this.nameEl.setText(text); 230 + return this; 231 + } 232 + setDesc(text) { 233 + this.descEl.setText(text); 234 + return this; 235 + } 236 + addText(callback) { 237 + const component = new TextComponent(this.controlEl); 238 + callback(component); 239 + return this; 240 + } 241 + addToggle(callback) { 242 + const component = new ToggleComponent(this.controlEl); 243 + callback(component); 244 + return this; 245 + } 246 + addDropdown(callback) { 247 + const component = new DropdownComponent(this.controlEl); 248 + callback(component); 249 + return this; 250 + } 251 + addButton(callback) { 252 + const component = new ButtonComponent(this.controlEl); 253 + callback(component); 254 + return this; 255 + } 256 + }; 257 + var TextComponent = class { 258 + constructor(containerEl) { 259 + this.el = containerEl.createEl("input", { 260 + attr: { type: "text" }, 261 + cls: "plugin-setting-text-input" 262 + }); 263 + } 264 + setValue(value) { 265 + this.el.setAttr("value", value == null ? "" : String(value)); 266 + return this; 267 + } 268 + setPlaceholder(value) { 269 + this.el.setAttr("placeholder", value); 270 + return this; 271 + } 272 + onChange(callback) { 273 + this.el.onChange((event) => callback(event.target.value)); 274 + return this; 275 + } 276 + }; 277 + var ToggleComponent = class { 278 + constructor(containerEl) { 279 + this.el = containerEl.createEl("input", { 280 + attr: { type: "checkbox" }, 281 + cls: "plugin-setting-toggle" 282 + }); 283 + } 284 + setValue(value) { 285 + if (value) this.el.setAttr("checked", ""); 286 + else delete this.el.attrs.checked; 287 + return this; 288 + } 289 + onChange(callback) { 290 + this.el.onChange((event) => callback(event.target.checked)); 291 + return this; 292 + } 293 + }; 294 + var DropdownComponent = class { 295 + constructor(containerEl) { 296 + this.el = containerEl.createEl("select", { 297 + cls: "plugin-setting-dropdown" 298 + }); 299 + } 300 + addOption(value, label) { 301 + this.el.createEl("option", { text: label, attr: { value } }); 302 + return this; 303 + } 304 + addOptions(map) { 305 + for (const [value, label] of Object.entries(map)) { 306 + this.addOption(value, label); 307 + } 308 + return this; 309 + } 310 + setValue(value) { 311 + for (const child of this.el.children) { 312 + if (child.attrs?.value === value) { 313 + child.attrs.selected = ""; 314 + } else if (child.attrs) { 315 + delete child.attrs.selected; 316 + } 317 + } 318 + return this; 319 + } 320 + onChange(callback) { 321 + this.el.onChange((event) => callback(event.target.value)); 322 + return this; 323 + } 324 + }; 325 + var ButtonComponent = class { 326 + constructor(containerEl) { 327 + this.el = containerEl.createEl("button", { 328 + cls: "plugin-setting-button" 329 + }); 330 + } 331 + setButtonText(text) { 332 + this.el.setText(text); 333 + return this; 334 + } 335 + setCta() { 336 + this.el.addClass("primary-button"); 337 + return this; 338 + } 339 + onClick(callback) { 340 + this.el.onClick(callback); 341 + return this; 342 + } 343 + }; 344 + var VirtualEl = class _VirtualEl { 345 + constructor(tag) { 346 + this.tag = tag; 347 + this.attrs = {}; 348 + this.text = null; 349 + this.children = []; 350 + this.events = {}; 351 + } 352 + onClick(fn) { 353 + const handlerId = uuid.create(); 354 + callHandlers.set(handlerId, fn); 355 + this.events.click = handlerId; 356 + return this; 357 + } 358 + onChange(fn) { 359 + const handlerId = uuid.create(); 360 + callHandlers.set(handlerId, fn); 361 + this.events.change = handlerId; 362 + return this; 363 + } 364 + onInput(fn) { 365 + const handlerId = uuid.create(); 366 + callHandlers.set(handlerId, fn); 367 + this.events.input = handlerId; 368 + return this; 369 + } 370 + setText(text) { 371 + this.text = text; 372 + this.children = []; 373 + return this; 374 + } 375 + empty() { 376 + this.text = null; 377 + this.children = []; 378 + return this; 379 + } 380 + addClass(cls) { 381 + this.attrs.class = this.attrs.class ? `${this.attrs.class} ${cls}` : cls; 382 + return this; 383 + } 384 + setAttr(name, value) { 385 + this.attrs[name] = value === void 0 ? "" : value; 386 + return this; 387 + } 388 + createEl(tag, options = {}, callback) { 389 + const child = new _VirtualEl(tag); 390 + if (options.text != null) child.text = options.text; 391 + if (options.cls) { 392 + child.attrs.class = Array.isArray(options.cls) ? options.cls.join(" ") : options.cls; 393 + } 394 + if (options.attr) Object.assign(child.attrs, options.attr); 395 + this.children.push(child); 396 + if (typeof callback === "function") callback(child); 397 + return child; 398 + } 399 + createDiv(options = {}, callback) { 400 + return this.createEl("div", options, callback); 401 + } 402 + createSpan(options = {}, callback) { 403 + return this.createEl("span", options, callback); 404 + } 405 + _serialize() { 406 + return { 407 + tag: this.tag, 408 + attrs: this.attrs, 409 + text: this.text, 410 + children: this.children.map((child) => child._serialize()), 411 + events: this.events 412 + }; 413 + } 414 + }; 415 + self.addEventListener("message", async (event) => { 416 + const message = event.data; 417 + if (!message || typeof message !== "object") return; 418 + if (message.type === "call") { 419 + const fn = callHandlers.get(message.handlerId); 420 + if (!fn) { 421 + self.postMessage({ 422 + type: "result", 423 + callId: message.callId, 424 + error: `unknown handler ${message.handlerId}` 425 + }); 426 + return; 427 + } 428 + try { 429 + const value = await fn(...message.args); 430 + self.postMessage({ type: "result", callId: message.callId, value }); 431 + } catch (error) { 432 + self.postMessage({ 433 + type: "result", 434 + callId: message.callId, 435 + error: error.message ?? String(error) 436 + }); 437 + } 438 + return; 439 + } 440 + if (message.type === "hostResult") { 441 + const pending = pendingHostCalls.get(message.hostCallId); 442 + if (!pending) return; 443 + pendingHostCalls.delete(message.hostCallId); 444 + if (message.error) pending.reject(new Error(message.error)); 445 + else pending.resolve(message.value); 446 + return; 447 + } 448 + if (message.type === "event") { 449 + switch (message.event) { 450 + case "modalDismissed": { 451 + const modal = openModals.get(message.data.modalId); 452 + if (modal) { 453 + openModals.delete(message.data.modalId); 454 + modal.onClose(); 455 + } 456 + return; 457 + } 458 + } 459 + return; 460 + } 461 + }); 462 + 463 + // src/main.js 464 + var DEFAULT_SETTINGS = { 465 + greeting: "Hello", 466 + loud: false, 467 + buttonStyle: "primary" 468 + }; 469 + var SampleModal = class extends Modal { 470 + constructor(settings) { 471 + super(); 472 + this.settings = settings; 473 + } 474 + onOpen() { 475 + this.titleEl.setText("Sample modal"); 476 + const message = this.settings.loud ? `${this.settings.greeting}!!!` : `${this.settings.greeting}.`; 477 + this.contentEl.createEl("p", { 478 + text: message, 479 + cls: "modal-dialog-message" 480 + }); 481 + const buttons = this.contentEl.createDiv({ cls: "modal-dialog-buttons" }); 482 + buttons.createEl("button", { 483 + text: "OK", 484 + cls: `modal-dialog-button ${this.settings.buttonStyle}-button` 485 + }).onClick(() => this.close()); 486 + } 487 + onClose() { 488 + this.titleEl.empty(); 489 + this.contentEl.empty(); 490 + } 491 + }; 492 + var SampleSettingTab = class extends PluginSettingTab { 493 + constructor() { 494 + super(); 495 + this.setName("Impro Sample Plugin"); 496 + } 497 + display() { 498 + new Setting(this.containerEl).setName("Greeting").setDesc("Text shown in the sample modal").addText( 499 + (text) => text.setPlaceholder("Hello").setValue(this.plugin.settings.greeting).onChange(async (value) => { 500 + this.plugin.settings.greeting = value; 501 + await this.plugin.saveData(this.plugin.settings); 502 + }) 503 + ); 504 + new Setting(this.containerEl).setName("Loud mode").setDesc("Adds exclamation marks to the greeting").addToggle( 505 + (toggle) => toggle.setValue(this.plugin.settings.loud).onChange(async (value) => { 506 + this.plugin.settings.loud = value; 507 + await this.plugin.saveData(this.plugin.settings); 508 + }) 509 + ); 510 + new Setting(this.containerEl).setName("Button style").setDesc("Modal button style").addDropdown( 511 + (dropdown) => dropdown.addOptions({ primary: "Primary", danger: "Danger" }).setValue(this.plugin.settings.buttonStyle).onChange(async (value) => { 512 + this.plugin.settings.buttonStyle = value; 513 + await this.plugin.saveData(this.plugin.settings); 514 + }) 515 + ); 516 + new Setting(this.containerEl).setName("Reset settings").setDesc("Reset all settings to defaults").addButton( 517 + (button) => button.setButtonText("Reset").onClick(async () => { 518 + this.plugin.settings = { ...DEFAULT_SETTINGS }; 519 + await this.plugin.saveData(this.plugin.settings); 520 + this.refresh(); 521 + }) 522 + ); 523 + } 524 + }; 525 + var ImproSamplePlugin = class extends Plugin { 526 + async onload() { 527 + const saved = await this.loadData(); 528 + this.settings = { ...DEFAULT_SETTINGS, ...saved ?? {} }; 529 + this.addSettingTab(new SampleSettingTab()); 530 + this.addSidebarItem("lightning-bolt", "Open modal", () => { 531 + new SampleModal(this.settings).open(); 532 + }); 533 + this.app.on("post-context-menu", (menu) => { 534 + menu.addItem( 535 + (item) => item.setTitle("Open modal").setIcon("lightning-bolt").onClick(() => { 536 + new SampleModal(this.settings).open(); 537 + }) 538 + ); 539 + }); 540 + this.onSettingsChange((data) => { 541 + this.settings = { ...DEFAULT_SETTINGS, ...data ?? {} }; 542 + }); 543 + } 544 + }; 545 + ImproSamplePlugin.register();
+7
manifest.json
··· 1 + { 2 + "id": "impro-sample-plugin", 3 + "name": "Impro Sample Plugin", 4 + "version": "0.1.0", 5 + "author": "Impro", 6 + "description": "Starter reference plugin demonstrating commands, modals, and the host bridge." 7 + }
+499
package-lock.json
··· 1 + { 2 + "name": "impro-sample-plugin", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "impro-sample-plugin", 9 + "version": "0.1.0", 10 + "devDependencies": { 11 + "esbuild": "^0.25.10" 12 + } 13 + }, 14 + "node_modules/@esbuild/aix-ppc64": { 15 + "version": "0.25.12", 16 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", 17 + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", 18 + "cpu": [ 19 + "ppc64" 20 + ], 21 + "dev": true, 22 + "license": "MIT", 23 + "optional": true, 24 + "os": [ 25 + "aix" 26 + ], 27 + "engines": { 28 + "node": ">=18" 29 + } 30 + }, 31 + "node_modules/@esbuild/android-arm": { 32 + "version": "0.25.12", 33 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", 34 + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", 35 + "cpu": [ 36 + "arm" 37 + ], 38 + "dev": true, 39 + "license": "MIT", 40 + "optional": true, 41 + "os": [ 42 + "android" 43 + ], 44 + "engines": { 45 + "node": ">=18" 46 + } 47 + }, 48 + "node_modules/@esbuild/android-arm64": { 49 + "version": "0.25.12", 50 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", 51 + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", 52 + "cpu": [ 53 + "arm64" 54 + ], 55 + "dev": true, 56 + "license": "MIT", 57 + "optional": true, 58 + "os": [ 59 + "android" 60 + ], 61 + "engines": { 62 + "node": ">=18" 63 + } 64 + }, 65 + "node_modules/@esbuild/android-x64": { 66 + "version": "0.25.12", 67 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", 68 + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", 69 + "cpu": [ 70 + "x64" 71 + ], 72 + "dev": true, 73 + "license": "MIT", 74 + "optional": true, 75 + "os": [ 76 + "android" 77 + ], 78 + "engines": { 79 + "node": ">=18" 80 + } 81 + }, 82 + "node_modules/@esbuild/darwin-arm64": { 83 + "version": "0.25.12", 84 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", 85 + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", 86 + "cpu": [ 87 + "arm64" 88 + ], 89 + "dev": true, 90 + "license": "MIT", 91 + "optional": true, 92 + "os": [ 93 + "darwin" 94 + ], 95 + "engines": { 96 + "node": ">=18" 97 + } 98 + }, 99 + "node_modules/@esbuild/darwin-x64": { 100 + "version": "0.25.12", 101 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", 102 + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", 103 + "cpu": [ 104 + "x64" 105 + ], 106 + "dev": true, 107 + "license": "MIT", 108 + "optional": true, 109 + "os": [ 110 + "darwin" 111 + ], 112 + "engines": { 113 + "node": ">=18" 114 + } 115 + }, 116 + "node_modules/@esbuild/freebsd-arm64": { 117 + "version": "0.25.12", 118 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", 119 + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", 120 + "cpu": [ 121 + "arm64" 122 + ], 123 + "dev": true, 124 + "license": "MIT", 125 + "optional": true, 126 + "os": [ 127 + "freebsd" 128 + ], 129 + "engines": { 130 + "node": ">=18" 131 + } 132 + }, 133 + "node_modules/@esbuild/freebsd-x64": { 134 + "version": "0.25.12", 135 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", 136 + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", 137 + "cpu": [ 138 + "x64" 139 + ], 140 + "dev": true, 141 + "license": "MIT", 142 + "optional": true, 143 + "os": [ 144 + "freebsd" 145 + ], 146 + "engines": { 147 + "node": ">=18" 148 + } 149 + }, 150 + "node_modules/@esbuild/linux-arm": { 151 + "version": "0.25.12", 152 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", 153 + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", 154 + "cpu": [ 155 + "arm" 156 + ], 157 + "dev": true, 158 + "license": "MIT", 159 + "optional": true, 160 + "os": [ 161 + "linux" 162 + ], 163 + "engines": { 164 + "node": ">=18" 165 + } 166 + }, 167 + "node_modules/@esbuild/linux-arm64": { 168 + "version": "0.25.12", 169 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", 170 + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", 171 + "cpu": [ 172 + "arm64" 173 + ], 174 + "dev": true, 175 + "license": "MIT", 176 + "optional": true, 177 + "os": [ 178 + "linux" 179 + ], 180 + "engines": { 181 + "node": ">=18" 182 + } 183 + }, 184 + "node_modules/@esbuild/linux-ia32": { 185 + "version": "0.25.12", 186 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", 187 + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", 188 + "cpu": [ 189 + "ia32" 190 + ], 191 + "dev": true, 192 + "license": "MIT", 193 + "optional": true, 194 + "os": [ 195 + "linux" 196 + ], 197 + "engines": { 198 + "node": ">=18" 199 + } 200 + }, 201 + "node_modules/@esbuild/linux-loong64": { 202 + "version": "0.25.12", 203 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", 204 + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", 205 + "cpu": [ 206 + "loong64" 207 + ], 208 + "dev": true, 209 + "license": "MIT", 210 + "optional": true, 211 + "os": [ 212 + "linux" 213 + ], 214 + "engines": { 215 + "node": ">=18" 216 + } 217 + }, 218 + "node_modules/@esbuild/linux-mips64el": { 219 + "version": "0.25.12", 220 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", 221 + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", 222 + "cpu": [ 223 + "mips64el" 224 + ], 225 + "dev": true, 226 + "license": "MIT", 227 + "optional": true, 228 + "os": [ 229 + "linux" 230 + ], 231 + "engines": { 232 + "node": ">=18" 233 + } 234 + }, 235 + "node_modules/@esbuild/linux-ppc64": { 236 + "version": "0.25.12", 237 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", 238 + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", 239 + "cpu": [ 240 + "ppc64" 241 + ], 242 + "dev": true, 243 + "license": "MIT", 244 + "optional": true, 245 + "os": [ 246 + "linux" 247 + ], 248 + "engines": { 249 + "node": ">=18" 250 + } 251 + }, 252 + "node_modules/@esbuild/linux-riscv64": { 253 + "version": "0.25.12", 254 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", 255 + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", 256 + "cpu": [ 257 + "riscv64" 258 + ], 259 + "dev": true, 260 + "license": "MIT", 261 + "optional": true, 262 + "os": [ 263 + "linux" 264 + ], 265 + "engines": { 266 + "node": ">=18" 267 + } 268 + }, 269 + "node_modules/@esbuild/linux-s390x": { 270 + "version": "0.25.12", 271 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", 272 + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", 273 + "cpu": [ 274 + "s390x" 275 + ], 276 + "dev": true, 277 + "license": "MIT", 278 + "optional": true, 279 + "os": [ 280 + "linux" 281 + ], 282 + "engines": { 283 + "node": ">=18" 284 + } 285 + }, 286 + "node_modules/@esbuild/linux-x64": { 287 + "version": "0.25.12", 288 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", 289 + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", 290 + "cpu": [ 291 + "x64" 292 + ], 293 + "dev": true, 294 + "license": "MIT", 295 + "optional": true, 296 + "os": [ 297 + "linux" 298 + ], 299 + "engines": { 300 + "node": ">=18" 301 + } 302 + }, 303 + "node_modules/@esbuild/netbsd-arm64": { 304 + "version": "0.25.12", 305 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", 306 + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", 307 + "cpu": [ 308 + "arm64" 309 + ], 310 + "dev": true, 311 + "license": "MIT", 312 + "optional": true, 313 + "os": [ 314 + "netbsd" 315 + ], 316 + "engines": { 317 + "node": ">=18" 318 + } 319 + }, 320 + "node_modules/@esbuild/netbsd-x64": { 321 + "version": "0.25.12", 322 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", 323 + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", 324 + "cpu": [ 325 + "x64" 326 + ], 327 + "dev": true, 328 + "license": "MIT", 329 + "optional": true, 330 + "os": [ 331 + "netbsd" 332 + ], 333 + "engines": { 334 + "node": ">=18" 335 + } 336 + }, 337 + "node_modules/@esbuild/openbsd-arm64": { 338 + "version": "0.25.12", 339 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", 340 + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", 341 + "cpu": [ 342 + "arm64" 343 + ], 344 + "dev": true, 345 + "license": "MIT", 346 + "optional": true, 347 + "os": [ 348 + "openbsd" 349 + ], 350 + "engines": { 351 + "node": ">=18" 352 + } 353 + }, 354 + "node_modules/@esbuild/openbsd-x64": { 355 + "version": "0.25.12", 356 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", 357 + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", 358 + "cpu": [ 359 + "x64" 360 + ], 361 + "dev": true, 362 + "license": "MIT", 363 + "optional": true, 364 + "os": [ 365 + "openbsd" 366 + ], 367 + "engines": { 368 + "node": ">=18" 369 + } 370 + }, 371 + "node_modules/@esbuild/openharmony-arm64": { 372 + "version": "0.25.12", 373 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", 374 + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", 375 + "cpu": [ 376 + "arm64" 377 + ], 378 + "dev": true, 379 + "license": "MIT", 380 + "optional": true, 381 + "os": [ 382 + "openharmony" 383 + ], 384 + "engines": { 385 + "node": ">=18" 386 + } 387 + }, 388 + "node_modules/@esbuild/sunos-x64": { 389 + "version": "0.25.12", 390 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", 391 + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", 392 + "cpu": [ 393 + "x64" 394 + ], 395 + "dev": true, 396 + "license": "MIT", 397 + "optional": true, 398 + "os": [ 399 + "sunos" 400 + ], 401 + "engines": { 402 + "node": ">=18" 403 + } 404 + }, 405 + "node_modules/@esbuild/win32-arm64": { 406 + "version": "0.25.12", 407 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", 408 + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", 409 + "cpu": [ 410 + "arm64" 411 + ], 412 + "dev": true, 413 + "license": "MIT", 414 + "optional": true, 415 + "os": [ 416 + "win32" 417 + ], 418 + "engines": { 419 + "node": ">=18" 420 + } 421 + }, 422 + "node_modules/@esbuild/win32-ia32": { 423 + "version": "0.25.12", 424 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", 425 + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", 426 + "cpu": [ 427 + "ia32" 428 + ], 429 + "dev": true, 430 + "license": "MIT", 431 + "optional": true, 432 + "os": [ 433 + "win32" 434 + ], 435 + "engines": { 436 + "node": ">=18" 437 + } 438 + }, 439 + "node_modules/@esbuild/win32-x64": { 440 + "version": "0.25.12", 441 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", 442 + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", 443 + "cpu": [ 444 + "x64" 445 + ], 446 + "dev": true, 447 + "license": "MIT", 448 + "optional": true, 449 + "os": [ 450 + "win32" 451 + ], 452 + "engines": { 453 + "node": ">=18" 454 + } 455 + }, 456 + "node_modules/esbuild": { 457 + "version": "0.25.12", 458 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", 459 + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", 460 + "dev": true, 461 + "hasInstallScript": true, 462 + "license": "MIT", 463 + "bin": { 464 + "esbuild": "bin/esbuild" 465 + }, 466 + "engines": { 467 + "node": ">=18" 468 + }, 469 + "optionalDependencies": { 470 + "@esbuild/aix-ppc64": "0.25.12", 471 + "@esbuild/android-arm": "0.25.12", 472 + "@esbuild/android-arm64": "0.25.12", 473 + "@esbuild/android-x64": "0.25.12", 474 + "@esbuild/darwin-arm64": "0.25.12", 475 + "@esbuild/darwin-x64": "0.25.12", 476 + "@esbuild/freebsd-arm64": "0.25.12", 477 + "@esbuild/freebsd-x64": "0.25.12", 478 + "@esbuild/linux-arm": "0.25.12", 479 + "@esbuild/linux-arm64": "0.25.12", 480 + "@esbuild/linux-ia32": "0.25.12", 481 + "@esbuild/linux-loong64": "0.25.12", 482 + "@esbuild/linux-mips64el": "0.25.12", 483 + "@esbuild/linux-ppc64": "0.25.12", 484 + "@esbuild/linux-riscv64": "0.25.12", 485 + "@esbuild/linux-s390x": "0.25.12", 486 + "@esbuild/linux-x64": "0.25.12", 487 + "@esbuild/netbsd-arm64": "0.25.12", 488 + "@esbuild/netbsd-x64": "0.25.12", 489 + "@esbuild/openbsd-arm64": "0.25.12", 490 + "@esbuild/openbsd-x64": "0.25.12", 491 + "@esbuild/openharmony-arm64": "0.25.12", 492 + "@esbuild/sunos-x64": "0.25.12", 493 + "@esbuild/win32-arm64": "0.25.12", 494 + "@esbuild/win32-ia32": "0.25.12", 495 + "@esbuild/win32-x64": "0.25.12" 496 + } 497 + } 498 + } 499 + }
+13
package.json
··· 1 + { 2 + "name": "impro-sample-plugin", 3 + "version": "0.1.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "build": "esbuild src/main.js --bundle --format=esm --outfile=main.js", 8 + "watch": "esbuild src/main.js --bundle --format=esm --outfile=main.js --watch" 9 + }, 10 + "devDependencies": { 11 + "esbuild": "^0.25.10" 12 + } 13 + }
+122
src/main.js
··· 1 + import { Modal, Plugin, PluginSettingTab, Setting } from "./pluginWorker.js"; 2 + 3 + const DEFAULT_SETTINGS = { 4 + greeting: "Hello", 5 + loud: false, 6 + buttonStyle: "primary", 7 + }; 8 + 9 + class SampleModal extends Modal { 10 + constructor(settings) { 11 + super(); 12 + this.settings = settings; 13 + } 14 + 15 + onOpen() { 16 + this.titleEl.setText("Sample modal"); 17 + const message = this.settings.loud 18 + ? `${this.settings.greeting}!!!` 19 + : `${this.settings.greeting}.`; 20 + this.contentEl.createEl("p", { 21 + text: message, 22 + cls: "modal-dialog-message", 23 + }); 24 + const buttons = this.contentEl.createDiv({ cls: "modal-dialog-buttons" }); 25 + buttons 26 + .createEl("button", { 27 + text: "OK", 28 + cls: `modal-dialog-button ${this.settings.buttonStyle}-button`, 29 + }) 30 + .onClick(() => this.close()); 31 + } 32 + 33 + onClose() { 34 + this.titleEl.empty(); 35 + this.contentEl.empty(); 36 + } 37 + } 38 + 39 + class SampleSettingTab extends PluginSettingTab { 40 + constructor() { 41 + super(); 42 + this.setName("Impro Sample Plugin"); 43 + } 44 + 45 + display() { 46 + new Setting(this.containerEl) 47 + .setName("Greeting") 48 + .setDesc("Text shown in the sample modal") 49 + .addText((text) => 50 + text 51 + .setPlaceholder("Hello") 52 + .setValue(this.plugin.settings.greeting) 53 + .onChange(async (value) => { 54 + this.plugin.settings.greeting = value; 55 + await this.plugin.saveData(this.plugin.settings); 56 + }), 57 + ); 58 + 59 + new Setting(this.containerEl) 60 + .setName("Loud mode") 61 + .setDesc("Adds exclamation marks to the greeting") 62 + .addToggle((toggle) => 63 + toggle.setValue(this.plugin.settings.loud).onChange(async (value) => { 64 + this.plugin.settings.loud = value; 65 + await this.plugin.saveData(this.plugin.settings); 66 + }), 67 + ); 68 + 69 + new Setting(this.containerEl) 70 + .setName("Button style") 71 + .setDesc("Modal button style") 72 + .addDropdown((dropdown) => 73 + dropdown 74 + .addOptions({ primary: "Primary", danger: "Danger" }) 75 + .setValue(this.plugin.settings.buttonStyle) 76 + .onChange(async (value) => { 77 + this.plugin.settings.buttonStyle = value; 78 + await this.plugin.saveData(this.plugin.settings); 79 + }), 80 + ); 81 + 82 + new Setting(this.containerEl) 83 + .setName("Reset settings") 84 + .setDesc("Reset all settings to defaults") 85 + .addButton((button) => 86 + button.setButtonText("Reset").onClick(async () => { 87 + this.plugin.settings = { ...DEFAULT_SETTINGS }; 88 + await this.plugin.saveData(this.plugin.settings); 89 + this.refresh(); 90 + }), 91 + ); 92 + } 93 + } 94 + 95 + class ImproSamplePlugin extends Plugin { 96 + async onload() { 97 + const saved = await this.loadData(); 98 + this.settings = { ...DEFAULT_SETTINGS, ...(saved ?? {}) }; 99 + 100 + this.addSettingTab(new SampleSettingTab()); 101 + 102 + this.addSidebarItem("lightning-bolt", "Open modal", () => { 103 + new SampleModal(this.settings).open(); 104 + }); 105 + this.app.on("post-context-menu", (menu) => { 106 + menu.addItem((item) => 107 + item 108 + .setTitle("Open modal") 109 + .setIcon("lightning-bolt") 110 + .onClick(() => { 111 + new SampleModal(this.settings).open(); 112 + }), 113 + ); 114 + }); 115 + 116 + this.onSettingsChange((data) => { 117 + this.settings = { ...DEFAULT_SETTINGS, ...(data ?? {}) }; 118 + }); 119 + } 120 + } 121 + 122 + ImproSamplePlugin.register();
+557
src/pluginWorker.js
··· 1 + // 0.0.1 2 + 3 + export class SimpleUUID { 4 + constructor() { 5 + this._id = 0; 6 + } 7 + create() { 8 + return this._id++; 9 + } 10 + } 11 + 12 + const uuid = new SimpleUUID(); 13 + 14 + const callHandlers = new Map(); 15 + 16 + const pendingHostCalls = new Map(); 17 + 18 + function hostCall(method, ...args) { 19 + const hostCallId = uuid.create(); 20 + return new Promise((resolve, reject) => { 21 + pendingHostCalls.set(hostCallId, { resolve, reject }); 22 + self.postMessage({ type: "hostCall", method, hostCallId, args }); 23 + }); 24 + } 25 + 26 + const eventListeners = new Map(); 27 + 28 + function addEventListener(event, listener) { 29 + let listeners = eventListeners.get(event); 30 + if (!listeners) { 31 + listeners = new Set(); 32 + eventListeners.set(event, listeners); 33 + const handlerId = uuid.create(); 34 + callHandlers.set(handlerId, async (...args) => { 35 + const menu = new Menu(); 36 + for (const eventListener of listeners) { 37 + try { 38 + await eventListener(menu, ...args); 39 + } catch (error) { 40 + console.error(`"${event}" listener threw:`, error); 41 + } 42 + } 43 + return menu._serialize(); 44 + }); 45 + self.postMessage({ 46 + type: "register", 47 + target: "eventListener", 48 + event, 49 + handlerId, 50 + }); 51 + } 52 + listeners.add(listener); 53 + } 54 + 55 + export class MenuItem { 56 + constructor() { 57 + this.title = ""; 58 + this.icon = null; 59 + this._callback = () => {}; 60 + } 61 + setTitle(title) { 62 + this.title = title; 63 + return this; 64 + } 65 + setIcon(icon) { 66 + this.icon = icon; 67 + return this; 68 + } 69 + onClick(callback) { 70 + this._callback = callback; 71 + return this; 72 + } 73 + } 74 + 75 + export class Menu { 76 + constructor() { 77 + this.items = []; 78 + } 79 + addItem(builder) { 80 + const item = new MenuItem(); 81 + builder(item); 82 + this.items.push(item); 83 + return this; 84 + } 85 + _serialize() { 86 + return this.items.map((item) => { 87 + const handlerId = uuid.create(); 88 + callHandlers.set(handlerId, item._callback); 89 + return { title: item.title, icon: item.icon, handlerId }; 90 + }); 91 + } 92 + } 93 + 94 + class App { 95 + constructor() { 96 + this.currentUser = null; 97 + } 98 + on(event, listener) { 99 + addEventListener(event, listener); 100 + } 101 + 102 + refreshFeedFilters(feedURI = null) { 103 + return hostCall("refreshFeedFilters", feedURI); 104 + } 105 + } 106 + 107 + export class Notice { 108 + constructor(message, timeout = 0) { 109 + this._toastId = uuid.create(); 110 + this._timeout = timeout; 111 + this._hidden = false; 112 + this.noticeEl = new VirtualEl("div"); 113 + this.noticeEl.addClass("toast"); 114 + this.noticeEl.setText(message); 115 + queueMicrotask(() => { 116 + if (this._hidden) return; 117 + hostCall("showToast", { 118 + toastId: this._toastId, 119 + element: this.noticeEl._serialize(), 120 + timeout: this._timeout, 121 + }); 122 + }); 123 + } 124 + setMessage(message) { 125 + this.noticeEl.setText(message); 126 + return this; 127 + } 128 + hide() { 129 + if (this._hidden) return; 130 + this._hidden = true; 131 + hostCall("hideToast", { toastId: this._toastId }); 132 + } 133 + } 134 + 135 + export class StyleSnippet { 136 + constructor(cssText) { 137 + this._snippetId = uuid.create(); 138 + this._removed = false; 139 + this.ready = new Promise((resolve, reject) => { 140 + queueMicrotask(() => { 141 + if (this._removed) return resolve(); 142 + hostCall("applyStyleSnippet", { 143 + snippetId: this._snippetId, 144 + cssText, 145 + }).then(resolve, reject); 146 + }); 147 + }); 148 + } 149 + remove() { 150 + if (this._removed) return; 151 + this._removed = true; 152 + hostCall("removeStyleSnippet", { snippetId: this._snippetId }); 153 + } 154 + } 155 + 156 + let registered = false; 157 + 158 + export class Plugin { 159 + constructor() { 160 + this.app = new App(); 161 + } 162 + 163 + addSidebarItem(icon, title, callback = () => {}) { 164 + const handlerId = uuid.create(); 165 + callHandlers.set(handlerId, callback); 166 + self.postMessage({ 167 + type: "register", 168 + target: "sidebarItem", 169 + icon, 170 + title, 171 + handlerId, 172 + }); 173 + } 174 + 175 + async loadData() { 176 + return hostCall("loadData"); 177 + } 178 + 179 + async saveData(data) { 180 + await hostCall("saveData", { data }); 181 + } 182 + 183 + addSettingTab(tab) { 184 + tab.plugin = this; 185 + const displayHandlerId = uuid.create(); 186 + callHandlers.set(displayHandlerId, () => { 187 + tab.containerEl = new VirtualEl("div"); 188 + tab.display(); 189 + return tab.containerEl._serialize(); 190 + }); 191 + self.postMessage({ 192 + type: "register", 193 + target: "settingTab", 194 + name: tab.name ?? null, 195 + displayHandlerId, 196 + }); 197 + this._settingTab = tab; 198 + } 199 + 200 + addFeedFilter(callback = () => {}) { 201 + const handlerId = uuid.create(); 202 + callHandlers.set(handlerId, callback); 203 + self.postMessage({ 204 + type: "register", 205 + target: "feedFilter", 206 + handlerId, 207 + }); 208 + } 209 + 210 + onload() {} 211 + onunload() {} 212 + 213 + static register() { 214 + if (registered) return; 215 + registered = true; 216 + const instance = new this(); 217 + hostCall("getCurrentUser") 218 + .then((user) => { 219 + instance.app.currentUser = user; 220 + return instance.onload(); 221 + }) 222 + .then( 223 + () => self.postMessage({ type: "ready" }), 224 + (error) => 225 + self.postMessage({ 226 + type: "ready", 227 + error: error?.message ?? String(error), 228 + }), 229 + ); 230 + } 231 + } 232 + 233 + const openModals = new Map(); 234 + 235 + export class Modal { 236 + constructor() { 237 + this._modalId = uuid.create(); 238 + this.contentEl = new VirtualEl("div"); 239 + this.titleEl = new VirtualEl("h2"); 240 + } 241 + 242 + open() { 243 + if (openModals.has(this._modalId)) return; 244 + openModals.set(this._modalId, this); 245 + this.onOpen(); 246 + self.postMessage({ 247 + type: "hostCall", 248 + method: "openModal", 249 + args: [ 250 + { 251 + modalId: this._modalId, 252 + title: this.titleEl._serialize(), 253 + content: this.contentEl._serialize(), 254 + }, 255 + ], 256 + }); 257 + } 258 + 259 + close() { 260 + if (!openModals.has(this._modalId)) return; 261 + openModals.delete(this._modalId); 262 + self.postMessage({ 263 + type: "hostCall", 264 + method: "closeModal", 265 + args: [{ modalId: this._modalId }], 266 + }); 267 + this.onClose(); 268 + } 269 + 270 + onOpen() {} 271 + onClose() {} 272 + } 273 + 274 + export class PluginSettingTab { 275 + constructor() { 276 + this.containerEl = new VirtualEl("div"); 277 + this.name = null; 278 + } 279 + setName(name) { 280 + this.name = name; 281 + return this; 282 + } 283 + display() {} 284 + refresh() { 285 + return hostCall("refreshSettingTab"); 286 + } 287 + } 288 + 289 + export class Setting { 290 + constructor(containerEl) { 291 + this.settingEl = containerEl.createDiv({ cls: "plugin-setting-item" }); 292 + this.infoEl = this.settingEl.createDiv({ cls: "plugin-setting-item-info" }); 293 + this.nameEl = this.infoEl.createDiv({ cls: "plugin-setting-item-name" }); 294 + this.descEl = this.infoEl.createDiv({ cls: "plugin-setting-item-desc" }); 295 + this.controlEl = this.settingEl.createDiv({ 296 + cls: "plugin-setting-item-control", 297 + }); 298 + } 299 + setName(text) { 300 + this.nameEl.setText(text); 301 + return this; 302 + } 303 + setDesc(text) { 304 + this.descEl.setText(text); 305 + return this; 306 + } 307 + addText(callback) { 308 + const component = new TextComponent(this.controlEl); 309 + callback(component); 310 + return this; 311 + } 312 + addToggle(callback) { 313 + const component = new ToggleComponent(this.controlEl); 314 + callback(component); 315 + return this; 316 + } 317 + addDropdown(callback) { 318 + const component = new DropdownComponent(this.controlEl); 319 + callback(component); 320 + return this; 321 + } 322 + addButton(callback) { 323 + const component = new ButtonComponent(this.controlEl); 324 + callback(component); 325 + return this; 326 + } 327 + } 328 + 329 + class TextComponent { 330 + constructor(containerEl) { 331 + this.el = containerEl.createEl("input", { 332 + attr: { type: "text" }, 333 + cls: "plugin-setting-text-input", 334 + }); 335 + } 336 + setValue(value) { 337 + this.el.setAttr("value", value == null ? "" : String(value)); 338 + return this; 339 + } 340 + setPlaceholder(value) { 341 + this.el.setAttr("placeholder", value); 342 + return this; 343 + } 344 + onChange(callback) { 345 + this.el.onChange((event) => callback(event.target.value)); 346 + return this; 347 + } 348 + } 349 + 350 + class ToggleComponent { 351 + constructor(containerEl) { 352 + this.el = containerEl.createEl("input", { 353 + attr: { type: "checkbox" }, 354 + cls: "plugin-setting-toggle", 355 + }); 356 + } 357 + setValue(value) { 358 + if (value) this.el.setAttr("checked", ""); 359 + else delete this.el.attrs.checked; 360 + return this; 361 + } 362 + onChange(callback) { 363 + this.el.onChange((event) => callback(event.target.checked)); 364 + return this; 365 + } 366 + } 367 + 368 + class DropdownComponent { 369 + constructor(containerEl) { 370 + this.el = containerEl.createEl("select", { 371 + cls: "plugin-setting-dropdown", 372 + }); 373 + } 374 + addOption(value, label) { 375 + this.el.createEl("option", { text: label, attr: { value } }); 376 + return this; 377 + } 378 + addOptions(map) { 379 + for (const [value, label] of Object.entries(map)) { 380 + this.addOption(value, label); 381 + } 382 + return this; 383 + } 384 + setValue(value) { 385 + for (const child of this.el.children) { 386 + if (child.attrs?.value === value) { 387 + child.attrs.selected = ""; 388 + } else if (child.attrs) { 389 + delete child.attrs.selected; 390 + } 391 + } 392 + return this; 393 + } 394 + onChange(callback) { 395 + this.el.onChange((event) => callback(event.target.value)); 396 + return this; 397 + } 398 + } 399 + 400 + class ButtonComponent { 401 + constructor(containerEl) { 402 + this.el = containerEl.createEl("button", { 403 + cls: "plugin-setting-button", 404 + }); 405 + } 406 + setButtonText(text) { 407 + this.el.setText(text); 408 + return this; 409 + } 410 + setCta() { 411 + this.el.addClass("primary-button"); 412 + return this; 413 + } 414 + onClick(callback) { 415 + this.el.onClick(callback); 416 + return this; 417 + } 418 + } 419 + 420 + class VirtualEl { 421 + constructor(tag) { 422 + this.tag = tag; 423 + this.attrs = {}; 424 + this.text = null; 425 + this.children = []; 426 + this.events = {}; 427 + } 428 + 429 + onClick(fn) { 430 + const handlerId = uuid.create(); 431 + callHandlers.set(handlerId, fn); 432 + this.events.click = handlerId; 433 + return this; 434 + } 435 + 436 + onChange(fn) { 437 + const handlerId = uuid.create(); 438 + callHandlers.set(handlerId, fn); 439 + this.events.change = handlerId; 440 + return this; 441 + } 442 + 443 + onInput(fn) { 444 + const handlerId = uuid.create(); 445 + callHandlers.set(handlerId, fn); 446 + this.events.input = handlerId; 447 + return this; 448 + } 449 + 450 + setText(text) { 451 + this.text = text; 452 + this.children = []; 453 + return this; 454 + } 455 + 456 + empty() { 457 + this.text = null; 458 + this.children = []; 459 + return this; 460 + } 461 + 462 + addClass(cls) { 463 + this.attrs.class = this.attrs.class ? `${this.attrs.class} ${cls}` : cls; 464 + return this; 465 + } 466 + 467 + setAttr(name, value) { 468 + this.attrs[name] = value === undefined ? "" : value; 469 + return this; 470 + } 471 + 472 + createEl(tag, options = {}, callback) { 473 + const child = new VirtualEl(tag); 474 + if (options.text != null) child.text = options.text; 475 + if (options.cls) { 476 + child.attrs.class = Array.isArray(options.cls) 477 + ? options.cls.join(" ") 478 + : options.cls; 479 + } 480 + if (options.attr) Object.assign(child.attrs, options.attr); 481 + this.children.push(child); 482 + if (typeof callback === "function") callback(child); 483 + return child; 484 + } 485 + 486 + createDiv(options = {}, callback) { 487 + return this.createEl("div", options, callback); 488 + } 489 + 490 + createSpan(options = {}, callback) { 491 + return this.createEl("span", options, callback); 492 + } 493 + 494 + _serialize() { 495 + return { 496 + tag: this.tag, 497 + attrs: this.attrs, 498 + text: this.text, 499 + children: this.children.map((child) => child._serialize()), 500 + events: this.events, 501 + }; 502 + } 503 + } 504 + 505 + self.addEventListener("message", async (event) => { 506 + const message = event.data; 507 + if (!message || typeof message !== "object") return; 508 + 509 + // RPC calls 510 + if (message.type === "call") { 511 + const fn = callHandlers.get(message.handlerId); 512 + if (!fn) { 513 + self.postMessage({ 514 + type: "result", 515 + callId: message.callId, 516 + error: `unknown handler ${message.handlerId}`, 517 + }); 518 + return; 519 + } 520 + try { 521 + const value = await fn(...message.args); 522 + self.postMessage({ type: "result", callId: message.callId, value }); 523 + } catch (error) { 524 + self.postMessage({ 525 + type: "result", 526 + callId: message.callId, 527 + error: error.message ?? String(error), 528 + }); 529 + } 530 + return; 531 + } 532 + 533 + // Host call results 534 + if (message.type === "hostResult") { 535 + const pending = pendingHostCalls.get(message.hostCallId); 536 + if (!pending) return; 537 + pendingHostCalls.delete(message.hostCallId); 538 + if (message.error) pending.reject(new Error(message.error)); 539 + else pending.resolve(message.value); 540 + return; 541 + } 542 + 543 + // Events 544 + if (message.type === "event") { 545 + switch (message.event) { 546 + case "modalDismissed": { 547 + const modal = openModals.get(message.data.modalId); 548 + if (modal) { 549 + openModals.delete(message.data.modalId); 550 + modal.onClose(); 551 + } 552 + return; 553 + } 554 + } 555 + return; 556 + } 557 + });