@drift-beacon/plugin 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- package/dist/internal/models.js +6 -0
- package/dist/internal/theme.js +75 -0
- package/dist/types.d.ts +63 -2
- package/dist/ui.d.ts +5 -1
- package/dist/ui.js +25 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -222,6 +222,8 @@ Settings items have `name` (unique), `title`, `type` and optional `description`
|
|
|
222
222
|
|
|
223
223
|
`connect()` performs a handshake with the web app and resolves to the UI `ctx`. It rejects with `unsupported` when the app doesn't run this UI's API version, and with `unavailable` outside Drift Beacon or after 10 seconds without an answer. `ctx.onDataChange` fires after every update (data, settings, storage, or other plugins' status and state; not their events).
|
|
224
224
|
|
|
225
|
+
The app runs a built UI in a sandboxed frame (`allow-scripts` only): it has no storage, cookies, popups or modal dialogs, and a `<form>` never submits. Handle Enter and your button's press yourself instead of relying on a form's `submit`.
|
|
226
|
+
|
|
225
227
|
With React, re-render on every update:
|
|
226
228
|
|
|
227
229
|
```tsx
|
|
@@ -256,6 +258,8 @@ Models keep their identity when their data changes: key `React.memo` and `useMem
|
|
|
256
258
|
const activity = ctx.activities.get(id);
|
|
257
259
|
activity?.category?.name; // links
|
|
258
260
|
activity?.isLive; // the current user has a live session of it
|
|
261
|
+
activity?.isPinned; // the current user has it pinned
|
|
262
|
+
activity?.goal; // { type: "duration", seconds } or { type: "count", count }, or null
|
|
259
263
|
await activity?.track(); // start a span / mark a point
|
|
260
264
|
activity?.data; // the plain row, a new frozen object whenever it changes
|
|
261
265
|
|
|
@@ -277,10 +281,22 @@ ctx.sessions.live({ mine: true });
|
|
|
277
281
|
{activity.iconPath && <svg viewBox="0 0 24 24" fill="currentColor"><path d={activity.iconPath} /></svg>}
|
|
278
282
|
```
|
|
279
283
|
|
|
284
|
+
- From 0.2.2, activities carry their goal, its period and their pins:
|
|
285
|
+
- `goal` is `{ type: "duration", seconds }` or `{ type: "count", count }`, or `null` when the activity has none. A point activity's goal always counts marks; a span activity's counts sessions or totals their duration, as the user chose.
|
|
286
|
+
- `period` is `"day"`, `"week"`, `"month"` or `"year"`: calendar periods from local midnight, weeks starting on Sunday. `null` means progress covers all history. Local means the browser's time zone in a UI, like the app's own progress, but the server's (its `TZ` environment variable) in main code: if the server runs in UTC and the user doesn't, main code's periods are off by the whole difference, all period long. Set `TZ` on the server to the household's zone.
|
|
287
|
+
- `pinnedBy` lists the users who have the activity pinned (each user pins at most one). Main code sees every user's pins, a UI only its own user's. `isPinned` is the current user's pin in both.
|
|
288
|
+
- Progress isn't included: add up `sessions()` in the current period. Drift Beacon counts every member's completed sessions from the period's start (a span counts on the day it started), plus the time so far of the current user's live session.
|
|
289
|
+
- A Drift Beacon older than 0.2.2 provides none of these, to main code as well as to UIs (main code uses the models of the Drift Beacon that runs it): `goal`, `period` and `pinnedBy` are `undefined`, which is why they are optional and how to tell (from 0.2.2, `pinnedBy` is always an array), and `isPinned` is `false` in a UI but `undefined` in main code. Test `isPinned` for truthiness.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
// The activity to show: the user's live one, else the one they pinned.
|
|
293
|
+
const shown = ctx.sessions.live({ mine: true })[0]?.activity ?? ctx.activities.list().find((a) => a.isPinned);
|
|
294
|
+
```
|
|
295
|
+
|
|
280
296
|
| Activity | |
|
|
281
297
|
|---|---|
|
|
282
|
-
| `id`, `name`, `description`, `trackingType`, `icon`, `iconPath`, `color`, `categoryId`, `archived`, `sortOrder`, `unit` | Fields (`color` is resolved from the category when needed) |
|
|
283
|
-
| `isSpan`, `isPoint`, `isLive`, `category` | Derived values and links |
|
|
298
|
+
| `id`, `name`, `description`, `trackingType`, `icon`, `iconPath`, `color`, `categoryId`, `archived`, `sortOrder`, `unit`, `goal`, `period`, `pinnedBy` | Fields (`color` is resolved from the category when needed) |
|
|
299
|
+
| `isSpan`, `isPoint`, `isLive`, `isPinned`, `category` | Derived values and links |
|
|
284
300
|
| `sessions(filter?)`, `live({ mine? })` | Its sessions |
|
|
285
301
|
| `start()`, `mark()`, `track()`, `end()` | Actions (`end()` ends the current user's live session of it) |
|
|
286
302
|
|
|
@@ -461,8 +477,25 @@ Users add the repository URL in **Plugins → Repositories** and install from th
|
|
|
461
477
|
|
|
462
478
|
- `apiVersion` in the manifest must match the SDK: `dbplugin`, `vite build` and the server all check it.
|
|
463
479
|
- Before 1.0, each minor version (`0.1`, `0.2`) may change the API, and a server runs only its exact version. Install the SDK with `@0.2` to stay on it.
|
|
464
|
-
- Patch releases (`0.2.x`) fix bugs and may add to API 0.2. An addition that needs the app, such as UI events in 0.2.1, does nothing on an older Drift Beacon. Ignore fields and string values you don't recognise (such as a new `trackingType`).
|
|
480
|
+
- Patch releases (`0.2.x`) fix bugs and may add to API 0.2. An addition that needs the app, such as UI events in 0.2.1 or activity goals and pins in 0.2.2, does nothing on an older Drift Beacon. Ignore fields and string values you don't recognise (such as a new `trackingType`).
|
|
465
481
|
|
|
466
482
|
## License
|
|
467
483
|
|
|
468
484
|
MIT
|
|
485
|
+
|
|
486
|
+
## Host theme (SDK 0.2.3)
|
|
487
|
+
|
|
488
|
+
`connect()` installs the host's theme as `--db-*` CSS variables on `<html>` before resolving. It keeps them synchronized without reloading the UI. It also sets `data-color-mode`, `color-scheme`, and the `light`/`dark` class. Use semantic tokens instead of copying the app's palette or forcing a dark class.
|
|
489
|
+
|
|
490
|
+
```css
|
|
491
|
+
body { background: var(--db-background); color: var(--db-foreground); }
|
|
492
|
+
.card { background: var(--db-surface); color: var(--db-surface-foreground); border: 1px solid var(--db-border); }
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
The tokens are `background`, `foreground`, `surface`, `surface-foreground`, `surface-raised`, `muted`, `border`, `accent`, `accent-foreground`, `danger`, `danger-foreground`, `success`, `success-foreground`, `warning`, `warning-foreground`, and `focus`, all prefixed with `--db-`. `background` is the containing surface; `surface` is a card; `surface-raised` is a more prominent surface. Values are complete CSS colors, not HSL channels. Activity and device colors remain domain data, separate from these UI colors.
|
|
496
|
+
|
|
497
|
+
`ctx.theme: UiTheme` is an immutable snapshot (`mode: "light" | "dark"`, `colors: UiThemeColors` using camelCase token names). `ctx.onThemeChange(callback): Unsubscribe` runs after CSS is updated, only when the theme changes. Canvas/chart renderers can subscribe here; theme-only updates do not fire `onDataChange`. Convert CSS colors to sRGB if the rendering library does not accept modern CSS color syntax.
|
|
498
|
+
|
|
499
|
+
Older hosts omit the theme. The SDK then supplies a stable dark fallback palette; an old-host reconnect restores that fallback. Missing theme fields in ordinary state updates leave the current theme unchanged. Invalid theme snapshots are ignored. Older UI bundles ignore the new fields and keep their existing appearance; rebuild and migrate their styles to adopt them.
|
|
500
|
+
|
|
501
|
+
Render the main UI after `connect()` resolves. Give loading/error UI fallback colors, and handle a rejected connection. Framework adapters should map their library's colors to these tokens; the SDK does not require React or HeroUI.
|
package/dist/internal/models.js
CHANGED
|
@@ -11,6 +11,9 @@ const ACTIVITY_FIELDS = [
|
|
|
11
11
|
"archived",
|
|
12
12
|
"sortOrder",
|
|
13
13
|
"unit",
|
|
14
|
+
"goal",
|
|
15
|
+
"period",
|
|
16
|
+
"pinnedBy",
|
|
14
17
|
];
|
|
15
18
|
const CATEGORY_FIELDS = [
|
|
16
19
|
"id",
|
|
@@ -61,6 +64,9 @@ class ActivityModel {
|
|
|
61
64
|
get isLive() {
|
|
62
65
|
return this.live({ mine: true }).length > 0;
|
|
63
66
|
}
|
|
67
|
+
get isPinned() {
|
|
68
|
+
return this.data.pinnedBy?.includes(this.#models.userId) ?? false;
|
|
69
|
+
}
|
|
64
70
|
get category() {
|
|
65
71
|
const categoryId = this.data.categoryId;
|
|
66
72
|
return categoryId === null ? undefined : this.#models.category(categoryId);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const THEME_COLOR_KEYS = [
|
|
2
|
+
"background",
|
|
3
|
+
"foreground",
|
|
4
|
+
"surface",
|
|
5
|
+
"surfaceForeground",
|
|
6
|
+
"surfaceRaised",
|
|
7
|
+
"muted",
|
|
8
|
+
"border",
|
|
9
|
+
"accent",
|
|
10
|
+
"accentForeground",
|
|
11
|
+
"danger",
|
|
12
|
+
"dangerForeground",
|
|
13
|
+
"success",
|
|
14
|
+
"successForeground",
|
|
15
|
+
"warning",
|
|
16
|
+
"warningForeground",
|
|
17
|
+
"focus",
|
|
18
|
+
];
|
|
19
|
+
export function fallbackTheme(mode = "dark") {
|
|
20
|
+
const dark = mode === "dark";
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
mode,
|
|
23
|
+
colors: Object.freeze({
|
|
24
|
+
background: dark ? "#09090b" : "#f8f8f8",
|
|
25
|
+
foreground: dark ? "#fafafa" : "#18181b",
|
|
26
|
+
surface: dark ? "#18181b" : "#ffffff",
|
|
27
|
+
surfaceForeground: dark ? "#fafafa" : "#18181b",
|
|
28
|
+
surfaceRaised: dark ? "#27272a" : "#f4f4f5",
|
|
29
|
+
muted: dark ? "#a1a1aa" : "#71717a",
|
|
30
|
+
border: dark ? "#3f3f46" : "#d4d4d8",
|
|
31
|
+
accent: "#2563eb",
|
|
32
|
+
accentForeground: "#ffffff",
|
|
33
|
+
danger: dark ? "#ef4444" : "#b91c1c",
|
|
34
|
+
dangerForeground: "#ffffff",
|
|
35
|
+
success: dark ? "#4ade80" : "#15803d",
|
|
36
|
+
successForeground: dark ? "#052e16" : "#ffffff",
|
|
37
|
+
warning: dark ? "#fbbf24" : "#a16207",
|
|
38
|
+
warningForeground: dark ? "#422006" : "#ffffff",
|
|
39
|
+
focus: "#3b82f6",
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function readTheme(value) {
|
|
44
|
+
if (!value || typeof value !== "object")
|
|
45
|
+
return;
|
|
46
|
+
const { mode, colors } = value;
|
|
47
|
+
if ((mode !== "light" && mode !== "dark") || !colors || typeof colors !== "object")
|
|
48
|
+
return;
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const key of THEME_COLOR_KEYS) {
|
|
51
|
+
const color = colors[key];
|
|
52
|
+
if (typeof color !== "string" ||
|
|
53
|
+
!color.trim() ||
|
|
54
|
+
color.length > 512 ||
|
|
55
|
+
/[;{}]/.test(color) ||
|
|
56
|
+
/(?:var|url)\s*\(/i.test(color))
|
|
57
|
+
return;
|
|
58
|
+
if (typeof CSS !== "undefined" && CSS.supports && !CSS.supports("color", color))
|
|
59
|
+
return;
|
|
60
|
+
result[key] = color;
|
|
61
|
+
}
|
|
62
|
+
return Object.freeze({ mode, colors: Object.freeze(result) });
|
|
63
|
+
}
|
|
64
|
+
export function applyTheme(theme) {
|
|
65
|
+
if (typeof document === "undefined")
|
|
66
|
+
return;
|
|
67
|
+
const root = document.documentElement;
|
|
68
|
+
for (const key of THEME_COLOR_KEYS) {
|
|
69
|
+
root.style.setProperty(`--db-${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`, theme.colors[key]);
|
|
70
|
+
}
|
|
71
|
+
root.dataset.colorMode = theme.mode;
|
|
72
|
+
root.style.colorScheme = theme.mode;
|
|
73
|
+
root.classList.toggle("dark", theme.mode === "dark");
|
|
74
|
+
root.classList.toggle("light", theme.mode === "light");
|
|
75
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,11 +4,25 @@
|
|
|
4
4
|
* Workspace data comes as models: an `Activity`, `Category` or `Session` with its fields, links,
|
|
5
5
|
* derived values and actions. Models read the latest snapshot the app sent, synchronously; there is
|
|
6
6
|
* one model per row for the life of a `ctx`, and `model.data` is its plain, frozen row. Later API
|
|
7
|
-
* versions may add fields and new values to string unions such as `trackingType`,
|
|
8
|
-
* ignore what they don't recognise.
|
|
7
|
+
* versions may add fields and new values to string unions such as `trackingType`, `goal.type` and
|
|
8
|
+
* `period`, so plugins must ignore what they don't recognise.
|
|
9
9
|
*/
|
|
10
10
|
/** Stops a subscription. Subscriptions made through `ctx` also stop when the instance stops. */
|
|
11
11
|
export type Unsubscribe = () => void;
|
|
12
|
+
/** An activity's goal: a total duration (seconds) or a number of times, per period. */
|
|
13
|
+
export type ActivityGoal = {
|
|
14
|
+
readonly type: "duration";
|
|
15
|
+
readonly seconds: number;
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: "count";
|
|
18
|
+
readonly count: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The calendar period a goal resets over, starting at local midnight (weeks start on Sunday). Local is the browser's
|
|
22
|
+
* time zone in a UI, like the app's own progress, but the server process's (its `TZ`) in main code, which can be hours
|
|
23
|
+
* off the user's for the whole period.
|
|
24
|
+
*/
|
|
25
|
+
export type GoalPeriod = "day" | "week" | "month" | "year";
|
|
12
26
|
/** An activity's plain row: what `activity.data` returns. */
|
|
13
27
|
export interface ActivityData {
|
|
14
28
|
readonly id: string;
|
|
@@ -25,6 +39,24 @@ export interface ActivityData {
|
|
|
25
39
|
/** Position within the activity's category. */
|
|
26
40
|
readonly sortOrder: number;
|
|
27
41
|
readonly unit: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* The activity's goal (from 0.2.2), or null when it has none. A point activity's goal is always a count of marks; a
|
|
44
|
+
* span activity's counts sessions or totals their duration, as the user chose. Progress isn't included: work it out
|
|
45
|
+
* from the sessions in the current `period`. `undefined` when the Drift Beacon running the plugin predates goals
|
|
46
|
+
* (main code and UIs alike).
|
|
47
|
+
*/
|
|
48
|
+
readonly goal?: ActivityGoal | null;
|
|
49
|
+
/**
|
|
50
|
+
* The period progress towards `goal` covers (from 0.2.2), or null when it covers all history. `undefined` when the
|
|
51
|
+
* Drift Beacon running the plugin predates goals.
|
|
52
|
+
*/
|
|
53
|
+
readonly period?: GoalPeriod | null;
|
|
54
|
+
/**
|
|
55
|
+
* Users who have this activity pinned, sorted (from 0.2.2); a user pins at most one activity. Main code sees every
|
|
56
|
+
* user's pins, a UI only its own user's: use `isPinned` for the current user. `undefined` when the Drift Beacon
|
|
57
|
+
* running the plugin predates pins.
|
|
58
|
+
*/
|
|
59
|
+
readonly pinnedBy?: readonly string[];
|
|
28
60
|
}
|
|
29
61
|
/** A category's plain row: what `category.data` returns. */
|
|
30
62
|
export interface CategoryData {
|
|
@@ -63,6 +95,11 @@ export interface Activity extends Readonly<ActivityData>, Model<ActivityData> {
|
|
|
63
95
|
readonly isPoint: boolean;
|
|
64
96
|
/** The current user has a live session of this activity. */
|
|
65
97
|
readonly isLive: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* The current user has this activity pinned (from 0.2.2). Test it for truthiness: main code gets its models from the
|
|
100
|
+
* Drift Beacon running it, and on one older than 0.2.2 this is `undefined` (a UI bundles its own models: false).
|
|
101
|
+
*/
|
|
102
|
+
readonly isPinned: boolean;
|
|
66
103
|
readonly category: Category | undefined;
|
|
67
104
|
/** This activity's sessions, newest first. */
|
|
68
105
|
sessions(filter?: Omit<SessionFilter, "activityId">): readonly Session[];
|
|
@@ -252,4 +289,28 @@ export interface PluginsApi {
|
|
|
252
289
|
/** This plugin itself, as other plugins see it. */
|
|
253
290
|
readonly self: PluginPeer;
|
|
254
291
|
}
|
|
292
|
+
/** Semantic colors supplied by the UI host (SDK 0.2.3). Values are complete CSS colors. */
|
|
293
|
+
export interface UiThemeColors {
|
|
294
|
+
readonly background: string;
|
|
295
|
+
readonly foreground: string;
|
|
296
|
+
readonly surface: string;
|
|
297
|
+
readonly surfaceForeground: string;
|
|
298
|
+
readonly surfaceRaised: string;
|
|
299
|
+
readonly muted: string;
|
|
300
|
+
readonly border: string;
|
|
301
|
+
readonly accent: string;
|
|
302
|
+
readonly accentForeground: string;
|
|
303
|
+
readonly danger: string;
|
|
304
|
+
readonly dangerForeground: string;
|
|
305
|
+
readonly success: string;
|
|
306
|
+
readonly successForeground: string;
|
|
307
|
+
readonly warning: string;
|
|
308
|
+
readonly warningForeground: string;
|
|
309
|
+
readonly focus: string;
|
|
310
|
+
}
|
|
311
|
+
/** An immutable theme snapshot. background is the surface containing this plugin. */
|
|
312
|
+
export interface UiTheme {
|
|
313
|
+
readonly mode: "light" | "dark";
|
|
314
|
+
readonly colors: Readonly<UiThemeColors>;
|
|
315
|
+
}
|
|
255
316
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { ActivitiesApi, CategoriesApi, PluginConfig, PluginInfo, PluginsApi, SessionsApi, StorageApi, Unsubscribe, UserInfo, WorkspaceInfo } from "./types.ts";
|
|
1
|
+
import type { ActivitiesApi, CategoriesApi, PluginConfig, PluginInfo, PluginsApi, SessionsApi, StorageApi, UiTheme, Unsubscribe, UserInfo, WorkspaceInfo } from "./types.ts";
|
|
2
2
|
export { PluginError } from "./errors.ts";
|
|
3
3
|
export type * from "./types.ts";
|
|
4
4
|
export { API_VERSION } from "./version.ts";
|
|
5
5
|
/** Everything a plugin UI can use. */
|
|
6
6
|
export interface UiContext {
|
|
7
|
+
/** Current host theme; SDK 0.2.3 applies --db-* variables before connect resolves. Older hosts use a dark fallback. */
|
|
8
|
+
readonly theme: UiTheme;
|
|
9
|
+
/** Theme-only updates do not fire onDataChange. CSS updates before this callback runs. */
|
|
10
|
+
onThemeChange(callback: (theme: UiTheme) => void): Unsubscribe;
|
|
7
11
|
/** The installed plugin; it can change while the UI is open, for example to a new version. */
|
|
8
12
|
readonly plugin: PluginInfo;
|
|
9
13
|
readonly user: UserInfo;
|
package/dist/ui.js
CHANGED
|
@@ -6,6 +6,7 @@ import { jsonEqual } from "./internal/json-equal.js";
|
|
|
6
6
|
import { WorkspaceModels } from "./internal/models.js";
|
|
7
7
|
import { commandArgumentsProblem, commandBudgetMs, eventNameProblem, samePluginStatus } from "./internal/peers.js";
|
|
8
8
|
import { isUiMessage, UI_CHANNEL, } from "./internal/protocol.js";
|
|
9
|
+
import { applyTheme, fallbackTheme, readTheme } from "./internal/theme.js";
|
|
9
10
|
import { API_VERSION } from "./version.js";
|
|
10
11
|
export { PluginError } from "./errors.js";
|
|
11
12
|
export { API_VERSION } from "./version.js";
|
|
@@ -80,6 +81,8 @@ function open(timeoutMs) {
|
|
|
80
81
|
class UiClient {
|
|
81
82
|
post;
|
|
82
83
|
context;
|
|
84
|
+
theme;
|
|
85
|
+
themeListeners = new Set();
|
|
83
86
|
config;
|
|
84
87
|
plugin;
|
|
85
88
|
rows = {
|
|
@@ -107,6 +110,8 @@ class UiClient {
|
|
|
107
110
|
peerObjects = new Map();
|
|
108
111
|
constructor(welcome, post) {
|
|
109
112
|
this.post = post;
|
|
113
|
+
this.theme = readTheme(welcome.theme) ?? fallbackTheme();
|
|
114
|
+
applyTheme(this.theme);
|
|
110
115
|
this.config = welcome.config;
|
|
111
116
|
this.plugin = welcome.plugin;
|
|
112
117
|
this.peersShared = welcome.peers !== undefined;
|
|
@@ -129,6 +134,10 @@ class UiClient {
|
|
|
129
134
|
const apis = this.models.apis();
|
|
130
135
|
const self = this;
|
|
131
136
|
this.context = {
|
|
137
|
+
get theme() {
|
|
138
|
+
return self.theme;
|
|
139
|
+
},
|
|
140
|
+
onThemeChange: (callback) => subscribe(self.themeListeners, callback),
|
|
132
141
|
get plugin() {
|
|
133
142
|
return self.plugin;
|
|
134
143
|
},
|
|
@@ -283,6 +292,7 @@ class UiClient {
|
|
|
283
292
|
receive(message) {
|
|
284
293
|
if (message.type === "welcome") {
|
|
285
294
|
this.plugin = message.plugin;
|
|
295
|
+
this.updateTheme(readTheme(message.theme) ?? fallbackTheme());
|
|
286
296
|
this.receive({
|
|
287
297
|
channel: message.channel,
|
|
288
298
|
type: "state",
|
|
@@ -310,6 +320,11 @@ class UiClient {
|
|
|
310
320
|
}
|
|
311
321
|
if (message.type !== "state")
|
|
312
322
|
return;
|
|
323
|
+
if (message.theme) {
|
|
324
|
+
const theme = readTheme(message.theme);
|
|
325
|
+
if (theme)
|
|
326
|
+
this.updateTheme(theme);
|
|
327
|
+
}
|
|
313
328
|
if (message.config)
|
|
314
329
|
this.config = message.config;
|
|
315
330
|
if (message.data)
|
|
@@ -321,7 +336,16 @@ class UiClient {
|
|
|
321
336
|
}
|
|
322
337
|
if (message.peers)
|
|
323
338
|
this.applyPeers(message.peers);
|
|
324
|
-
|
|
339
|
+
if (message.config || message.data || message.storage || message.peers) {
|
|
340
|
+
notify(this.dataListeners, (listener) => listener());
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
updateTheme(theme) {
|
|
344
|
+
if (jsonEqual(this.theme, theme))
|
|
345
|
+
return;
|
|
346
|
+
this.theme = theme;
|
|
347
|
+
applyTheme(theme);
|
|
348
|
+
notify(this.themeListeners, (listener) => listener(theme));
|
|
325
349
|
}
|
|
326
350
|
applyEvent({ plugin, event, payload }) {
|
|
327
351
|
if (!this.listed.has(plugin))
|
package/package.json
CHANGED