@flighthq/tray 0.3.0-next.906.07cea63 → 0.3.0
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/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableTrayGuards.d.ts +17 -0
- package/dist/enableTrayGuards.d.ts.map +1 -0
- package/dist/enableTrayGuards.js +32 -0
- package/dist/enableTrayGuards.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tray.d.ts +6 -0
- package/dist/tray.d.ts.map +1 -1
- package/dist/tray.js +57 -7
- package/dist/tray.js.map +1 -1
- package/package.json +5 -2
- package/src/enableTrayGuards.test.ts +79 -0
- package/src/tray.test.ts +184 -0
package/dist/contract.d.ts
CHANGED
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,QAAQ,CAAC"}
|
package/dist/contract.js
CHANGED
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,QAAQ,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Uninstalls the guard installed by `enableTrayGuards`. */
|
|
2
|
+
export declare function disableTrayGuards(): void;
|
|
3
|
+
/**
|
|
4
|
+
* Installs the caller-facing tray guard (opt-in, dev-only). It warns once — through `@flighthq/log` —
|
|
5
|
+
* when `startTrayIconAnimation` is given a non-positive interval.
|
|
6
|
+
*
|
|
7
|
+
* A `setInterval` of zero or less does not fail; it schedules as fast as the host will run it, writing
|
|
8
|
+
* the tray icon every tick. The animation "works", the app burns a core and hammers the platform's tray
|
|
9
|
+
* API, and nothing points at the call that asked for it. That is caller misuse rather than an expected
|
|
10
|
+
* failure, so it warns rather than throwing or returning a sentinel — the animation still starts, and
|
|
11
|
+
* the developer gets told why their machine is busy.
|
|
12
|
+
*
|
|
13
|
+
* Not importing this module costs production nothing: the message and the `@flighthq/log` dependency
|
|
14
|
+
* live only here.
|
|
15
|
+
*/
|
|
16
|
+
export declare function enableTrayGuards(): void;
|
|
17
|
+
//# sourceMappingURL=enableTrayGuards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableTrayGuards.d.ts","sourceRoot":"","sources":["../src/enableTrayGuards.ts"],"names":[],"mappings":"AAMA,4DAA4D;AAC5D,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { logOnce } from '@flighthq/log/contract';
|
|
2
|
+
import { LogLevel } from '@flighthq/types/contract';
|
|
3
|
+
import { setTrayAnimationGuard } from './tray';
|
|
4
|
+
/** Uninstalls the guard installed by `enableTrayGuards`. */
|
|
5
|
+
export function disableTrayGuards() {
|
|
6
|
+
setTrayAnimationGuard(null);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Installs the caller-facing tray guard (opt-in, dev-only). It warns once — through `@flighthq/log` —
|
|
10
|
+
* when `startTrayIconAnimation` is given a non-positive interval.
|
|
11
|
+
*
|
|
12
|
+
* A `setInterval` of zero or less does not fail; it schedules as fast as the host will run it, writing
|
|
13
|
+
* the tray icon every tick. The animation "works", the app burns a core and hammers the platform's tray
|
|
14
|
+
* API, and nothing points at the call that asked for it. That is caller misuse rather than an expected
|
|
15
|
+
* failure, so it warns rather than throwing or returning a sentinel — the animation still starts, and
|
|
16
|
+
* the developer gets told why their machine is busy.
|
|
17
|
+
*
|
|
18
|
+
* Not importing this module costs production nothing: the message and the `@flighthq/log` dependency
|
|
19
|
+
* live only here.
|
|
20
|
+
*/
|
|
21
|
+
export function enableTrayGuards() {
|
|
22
|
+
setTrayAnimationGuard(warnOnUnboundedTrayAnimation);
|
|
23
|
+
}
|
|
24
|
+
function warnOnUnboundedTrayAnimation(_tray, _frameCount, intervalMs) {
|
|
25
|
+
if (intervalMs > 0)
|
|
26
|
+
return;
|
|
27
|
+
logOnce('tray:non-positive-animation-interval', LogLevel.Warn, {
|
|
28
|
+
message: `startTrayIconAnimation: intervalMs is ${intervalMs}, so the icon will be rewritten as fast as the ` +
|
|
29
|
+
'host schedules timers rather than on a frame interval. Pass a positive millisecond interval.',
|
|
30
|
+
}, 'tray');
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=enableTrayGuards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableTrayGuards.js","sourceRoot":"","sources":["../src/enableTrayGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAE/C,4DAA4D;AAC5D,MAAM,UAAU,iBAAiB;IAC/B,qBAAqB,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB;IAC9B,qBAAqB,CAAC,4BAA4B,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,4BAA4B,CAAC,KAAe,EAAE,WAAmB,EAAE,UAAkB;IAC5F,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO;IAC3B,OAAO,CACL,sCAAsC,EACtC,QAAQ,CAAC,IAAI,EACb;QACE,OAAO,EACL,yCAAyC,UAAU,iDAAiD;YACpG,8FAA8F;KACjG,EACD,MAAM,CACP,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createTrayIcon, destroyTrayIcon, displayTrayBalloon, getTrayCapabilities, getTrayIconBounds, getTrayIconTitle, getTrayIconTooltip, getTrayIcons, isTrayDestroyed, onTrayEvent, popupTrayContextMenu, removeTrayBalloon, setTrayIcon, setTrayIconContextMenu, setTrayIconTemplate, setTrayIconTitle, setTrayIconTooltip, setTrayIgnoreDoubleClickEvents, setTrayPressedIcon, startTrayIconAnimation, } from './contract';
|
|
1
|
+
export { disableTrayGuards, enableTrayGuards, createTrayIcon, destroyTrayIcon, displayTrayBalloon, getTrayCapabilities, getTrayIconBounds, getTrayIconTitle, getTrayIconTooltip, getTrayIcons, isTrayDestroyed, isTrayIconAnimating, onTrayEvent, popupTrayContextMenu, removeTrayBalloon, setTrayIcon, setTrayIconContextMenu, setTrayIconTemplate, setTrayIconTitle, setTrayIconTooltip, setTrayIgnoreDoubleClickEvents, setTrayPressedIcon, startTrayIconAnimation, stopTrayIconAnimation, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,8BAA8B,EAC9B,kBAAkB,EAClB,sBAAsB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,8BAA8B,EAC9B,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createTrayIcon, destroyTrayIcon, displayTrayBalloon, getTrayCapabilities, getTrayIconBounds, getTrayIconTitle, getTrayIconTooltip, getTrayIcons, isTrayDestroyed, onTrayEvent, popupTrayContextMenu, removeTrayBalloon, setTrayIcon, setTrayIconContextMenu, setTrayIconTemplate, setTrayIconTitle, setTrayIconTooltip, setTrayIgnoreDoubleClickEvents, setTrayPressedIcon, startTrayIconAnimation, } from './contract';
|
|
1
|
+
export { disableTrayGuards, enableTrayGuards, createTrayIcon, destroyTrayIcon, displayTrayBalloon, getTrayCapabilities, getTrayIconBounds, getTrayIconTitle, getTrayIconTooltip, getTrayIcons, isTrayDestroyed, isTrayIconAnimating, onTrayEvent, popupTrayContextMenu, removeTrayBalloon, setTrayIcon, setTrayIconContextMenu, setTrayIconTemplate, setTrayIconTitle, setTrayIconTooltip, setTrayIgnoreDoubleClickEvents, setTrayPressedIcon, startTrayIconAnimation, stopTrayIconAnimation, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,8BAA8B,EAC9B,kBAAkB,EAClB,sBAAsB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,8BAA8B,EAC9B,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
package/dist/tray.d.ts
CHANGED
|
@@ -11,9 +11,14 @@ export declare function getTrayIcons(): readonly TrayIcon[];
|
|
|
11
11
|
export declare function getTrayIconTitle(tray: TrayIcon): string;
|
|
12
12
|
export declare function getTrayIconTooltip(tray: TrayIcon): string;
|
|
13
13
|
export declare function isTrayDestroyed(tray: TrayIcon): boolean;
|
|
14
|
+
export declare function isTrayIconAnimating(tray: TrayIcon): boolean;
|
|
14
15
|
export declare function onTrayEvent(listener: (event: Readonly<TrayEventData>) => void): () => void;
|
|
15
16
|
export declare function popupTrayContextMenu(tray: TrayIcon, position?: Readonly<Vector2Like>): void;
|
|
16
17
|
export declare function removeTrayBalloon(tray: TrayIcon): void;
|
|
18
|
+
/** Installs the tray guard, or clears it with `null`. The seam exists so the messages and the
|
|
19
|
+
* `@flighthq/log` dependency live in the separately-importable guard module rather than here; not
|
|
20
|
+
* importing that module costs production nothing. Called by `enableTrayGuards`, not directly. */
|
|
21
|
+
export declare function setTrayAnimationGuard(guard: ((tray: TrayIcon, frameCount: number, intervalMs: number) => void) | null): void;
|
|
17
22
|
export declare function setTrayBackend(backend: TrayBackend | null): void;
|
|
18
23
|
export declare function setTrayIcon(tray: TrayIcon, icon: string): void;
|
|
19
24
|
export declare function setTrayIconContextMenu(tray: TrayIcon, items: readonly MenuItemTemplate[]): void;
|
|
@@ -23,4 +28,5 @@ export declare function setTrayIconTooltip(tray: TrayIcon, tooltip: string): voi
|
|
|
23
28
|
export declare function setTrayIgnoreDoubleClickEvents(tray: TrayIcon, ignore: boolean): void;
|
|
24
29
|
export declare function setTrayPressedIcon(tray: TrayIcon, icon: string): void;
|
|
25
30
|
export declare function startTrayIconAnimation(tray: TrayIcon, frames: readonly string[], intervalMs: number): () => void;
|
|
31
|
+
export declare function stopTrayIconAnimation(tray: TrayIcon): void;
|
|
26
32
|
//# sourceMappingURL=tray.d.ts.map
|
package/dist/tray.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tray.d.ts","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,QAAQ,EACR,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"tray.d.ts","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,QAAQ,EACR,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAgB5D,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,QAAQ,GAAG,IAAI,CAGnF;AAKD,wBAAgB,oBAAoB,IAAI,WAAW,CAmElD;AAOD,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAGpD;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAE9F;AAGD,wBAAgB,cAAc,IAAI,WAAW,CAG5C;AAKD,wBAAgB,mBAAmB,IAAI,QAAQ,CAAC,gBAAgB,CAAC,CAEhE;AAID,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,IAAI,CAEhF;AAGD,wBAAgB,YAAY,IAAI,SAAS,QAAQ,EAAE,CAIlD;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEvD;AAGD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEzD;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAEvD;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAE3D;AAKD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAE1F;AAKD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAE3F;AAGD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAEtD;AAED;;kGAEkG;AAClG,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GAC/E,IAAI,CAEN;AAGD,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAEhE;AAID,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9D;AAID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAE/F;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAG,IAAI,CAE7E;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpE;AAGD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAExE;AAID,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAEpF;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAErE;AAcD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,IAAI,CAiBhH;AAcD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAK1D"}
|
package/dist/tray.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// Web tray capability constants. Web has no system tray — all capabilities are false.
|
|
1
|
+
// Web tray capability constants. Web has no system tray — all capabilities are false. Readonly and
|
|
2
|
+
// shared: getCapabilities hands this same object to every caller, so a mutable one would let any of
|
|
3
|
+
// them rewrite what the backend reports to all the others.
|
|
2
4
|
const WEB_CAPABILITIES = {
|
|
3
5
|
balloon: false,
|
|
4
6
|
bounds: false,
|
|
@@ -85,7 +87,12 @@ export function createWebTrayBackend() {
|
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
89
|
// Destroys a tray icon and frees its host resource. No-op when the host has no tray.
|
|
90
|
+
// Any icon animation running on this tray is stopped first: the interval writes through setTrayIcon,
|
|
91
|
+
// and left running it would keep calling into a resource the host has freed — forever, since the
|
|
92
|
+
// timer holds its own closure alive. Destroying the thing being animated is the clearest possible
|
|
93
|
+
// signal that the animation is over, so the caller does not have to have kept the stop function.
|
|
88
94
|
export function destroyTrayIcon(tray) {
|
|
95
|
+
stopTrayIconAnimation(tray);
|
|
89
96
|
getTrayBackend().destroy(tray.id);
|
|
90
97
|
}
|
|
91
98
|
// Displays a Windows balloon notification from the tray icon. No-op on macOS/Linux and on web.
|
|
@@ -129,6 +136,11 @@ export function getTrayIconTooltip(tray) {
|
|
|
129
136
|
export function isTrayDestroyed(tray) {
|
|
130
137
|
return getTrayBackend().isDestroyed(tray.id);
|
|
131
138
|
}
|
|
139
|
+
// True while an icon animation started by startTrayIconAnimation is running on this tray. False once
|
|
140
|
+
// it is stopped, replaced by a later start, or the tray is destroyed.
|
|
141
|
+
export function isTrayIconAnimating(tray) {
|
|
142
|
+
return _animations.has(tray.id);
|
|
143
|
+
}
|
|
132
144
|
// Subscribes to tray icon events, delivering a rich TrayEventData payload (id, type, bounds,
|
|
133
145
|
// position, modifier keys, and drop payloads). Returns an unsubscribe function. On web this
|
|
134
146
|
// never fires (no tray); a native host is required.
|
|
@@ -145,6 +157,12 @@ export function popupTrayContextMenu(tray, position) {
|
|
|
145
157
|
export function removeTrayBalloon(tray) {
|
|
146
158
|
getTrayBackend().removeBalloon(tray.id);
|
|
147
159
|
}
|
|
160
|
+
/** Installs the tray guard, or clears it with `null`. The seam exists so the messages and the
|
|
161
|
+
* `@flighthq/log` dependency live in the separately-importable guard module rather than here; not
|
|
162
|
+
* importing that module costs production nothing. Called by `enableTrayGuards`, not directly. */
|
|
163
|
+
export function setTrayAnimationGuard(guard) {
|
|
164
|
+
_animationGuard = guard;
|
|
165
|
+
}
|
|
148
166
|
// Installs a native host tray backend; pass null to fall back to the web default.
|
|
149
167
|
export function setTrayBackend(backend) {
|
|
150
168
|
_backend = backend;
|
|
@@ -183,21 +201,53 @@ export function setTrayIgnoreDoubleClickEvents(tray, ignore) {
|
|
|
183
201
|
export function setTrayPressedIcon(tray, icon) {
|
|
184
202
|
getTrayBackend().setPressedIcon(tray.id, icon);
|
|
185
203
|
}
|
|
186
|
-
// Starts an animated icon sequence by cycling through the given frames at the specified interval
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
204
|
+
// Starts an animated icon sequence by cycling through the given frames at the specified interval,
|
|
205
|
+
// returning a stop function. An empty frame list is a no-op. The tray icon is not destroyed when the
|
|
206
|
+
// animation stops, and the last frame shown stays.
|
|
207
|
+
//
|
|
208
|
+
// A tray icon has one image, so it has one animation: starting a second on the same tray replaces the
|
|
209
|
+
// first rather than running both. Without that, a perfectly ordinary sequence — swap a "syncing"
|
|
210
|
+
// animation for an "error" one — left two intervals writing to the same icon on the same tick, and
|
|
211
|
+
// the only handle to the orphaned one was a stop function the caller had no reason to still be
|
|
212
|
+
// holding. The returned function stops only the animation it started, so calling a stale one cannot
|
|
213
|
+
// cancel a newer animation.
|
|
214
|
+
//
|
|
190
215
|
// Note: interval timing is best-effort; the actual frame rate depends on the host event loop.
|
|
191
216
|
export function startTrayIconAnimation(tray, frames, intervalMs) {
|
|
192
217
|
if (frames.length === 0)
|
|
193
|
-
return
|
|
218
|
+
return _noopStop;
|
|
219
|
+
_animationGuard?.(tray, frames.length, intervalMs);
|
|
220
|
+
stopTrayIconAnimation(tray);
|
|
194
221
|
let index = 0;
|
|
195
222
|
setTrayIcon(tray, frames[index]);
|
|
196
223
|
const handle = setInterval(() => {
|
|
197
224
|
index = (index + 1) % frames.length;
|
|
198
225
|
setTrayIcon(tray, frames[index]);
|
|
199
226
|
}, intervalMs);
|
|
200
|
-
|
|
227
|
+
_animations.set(tray.id, handle);
|
|
228
|
+
return () => {
|
|
229
|
+
// Only clear if this animation is still the current one; a later start already replaced it.
|
|
230
|
+
if (_animations.get(tray.id) !== handle)
|
|
231
|
+
return;
|
|
232
|
+
clearInterval(handle);
|
|
233
|
+
_animations.delete(tray.id);
|
|
234
|
+
};
|
|
201
235
|
}
|
|
202
236
|
let _backend = null;
|
|
237
|
+
// The interval per animating tray id. Module-scoped like the backend, and correct at that scope: an
|
|
238
|
+
// id is the host's, so one id is one icon is one animation.
|
|
239
|
+
const _animations = new Map();
|
|
240
|
+
function _noopStop() { }
|
|
241
|
+
// Stops the icon animation running on this tray, if any. Idempotent, and safe on a tray that never
|
|
242
|
+
// animated. The named counterpart to startTrayIconAnimation for callers that hold the TrayIcon but
|
|
243
|
+
// not the stop function it returned — which is most of them, since the handle is what gets passed
|
|
244
|
+
// around and the closure is what gets dropped.
|
|
245
|
+
export function stopTrayIconAnimation(tray) {
|
|
246
|
+
const handle = _animations.get(tray.id);
|
|
247
|
+
if (handle === undefined)
|
|
248
|
+
return;
|
|
249
|
+
clearInterval(handle);
|
|
250
|
+
_animations.delete(tray.id);
|
|
251
|
+
}
|
|
252
|
+
let _animationGuard = null;
|
|
203
253
|
//# sourceMappingURL=tray.js.map
|
package/dist/tray.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tray.js","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"AAYA,
|
|
1
|
+
{"version":3,"file":"tray.js","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/tray",
|
|
3
|
-
"version": "0.3.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,7 +38,8 @@
|
|
|
36
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/log": "0.3.0",
|
|
42
|
+
"@flighthq/types": "0.3.0"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
45
|
"typescript": "^5.3.0"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { setLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import type { LogEntry } from '@flighthq/types/contract';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { disableTrayGuards, enableTrayGuards } from './enableTrayGuards';
|
|
6
|
+
import { createTrayIcon, setTrayBackend, startTrayIconAnimation, stopTrayIconAnimation } from './tray';
|
|
7
|
+
|
|
8
|
+
let entries: LogEntry[];
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.useFakeTimers();
|
|
12
|
+
entries = [];
|
|
13
|
+
setLogSink((entry) => entries.push(entry));
|
|
14
|
+
setTrayBackend({
|
|
15
|
+
create: () => 1,
|
|
16
|
+
destroy: () => {},
|
|
17
|
+
getBounds: () => null,
|
|
18
|
+
getCapabilities: () => ({ animation: true, badge: false, bounds: true, contextMenu: true, title: true }),
|
|
19
|
+
getTitle: () => '',
|
|
20
|
+
isDestroyed: () => false,
|
|
21
|
+
listIds: () => [1],
|
|
22
|
+
setContextMenu: () => {},
|
|
23
|
+
setIcon: () => {},
|
|
24
|
+
setPressedIcon: () => {},
|
|
25
|
+
setTitle: () => {},
|
|
26
|
+
setTooltip: () => {},
|
|
27
|
+
subscribe: () => () => {},
|
|
28
|
+
} as never);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
disableTrayGuards();
|
|
33
|
+
setTrayBackend(null);
|
|
34
|
+
setLogSink(null);
|
|
35
|
+
vi.useRealTimers();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function messages(): string {
|
|
39
|
+
return entries.map((e) => String((e.data as { message?: unknown } | undefined)?.message ?? '')).join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('disableTrayGuards', () => {
|
|
43
|
+
it('stops the guard inspecting later animations', () => {
|
|
44
|
+
enableTrayGuards();
|
|
45
|
+
disableTrayGuards();
|
|
46
|
+
const tray = createTrayIcon()!;
|
|
47
|
+
startTrayIconAnimation(tray, ['a', 'b'], 0);
|
|
48
|
+
stopTrayIconAnimation(tray);
|
|
49
|
+
expect(messages()).toBe('');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('enableTrayGuards', () => {
|
|
54
|
+
it('says nothing for a positive interval', () => {
|
|
55
|
+
enableTrayGuards();
|
|
56
|
+
const tray = createTrayIcon()!;
|
|
57
|
+
startTrayIconAnimation(tray, ['a', 'b'], 100);
|
|
58
|
+
stopTrayIconAnimation(tray);
|
|
59
|
+
expect(messages()).toBe('');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// A non-positive interval does not fail — setInterval simply schedules as fast as the host will run
|
|
63
|
+
// it, so the animation "works" while burning a core. Nothing points at the call that asked for it.
|
|
64
|
+
it('warns when the interval is zero, and still starts the animation', () => {
|
|
65
|
+
enableTrayGuards();
|
|
66
|
+
const tray = createTrayIcon()!;
|
|
67
|
+
|
|
68
|
+
const stop = startTrayIconAnimation(tray, ['a', 'b'], 0);
|
|
69
|
+
|
|
70
|
+
expect(messages()).toContain('intervalMs is 0');
|
|
71
|
+
// The wording that explains WHY is asserted here rather than in a separate negative-interval test:
|
|
72
|
+
// logOnce suppresses a key for the process, so a second test tripping this key would pass or fail
|
|
73
|
+
// on file order alone.
|
|
74
|
+
expect(messages()).toContain('as fast as the host schedules');
|
|
75
|
+
// Warned, not refused: this is misuse, not an expected failure.
|
|
76
|
+
expect(typeof stop).toBe('function');
|
|
77
|
+
stop();
|
|
78
|
+
});
|
|
79
|
+
});
|
package/src/tray.test.ts
CHANGED
|
@@ -20,9 +20,11 @@ import {
|
|
|
20
20
|
getTrayIconTitle,
|
|
21
21
|
getTrayIconTooltip,
|
|
22
22
|
isTrayDestroyed,
|
|
23
|
+
isTrayIconAnimating,
|
|
23
24
|
onTrayEvent,
|
|
24
25
|
popupTrayContextMenu,
|
|
25
26
|
removeTrayBalloon,
|
|
27
|
+
setTrayAnimationGuard,
|
|
26
28
|
setTrayBackend,
|
|
27
29
|
setTrayIcon,
|
|
28
30
|
setTrayIconContextMenu,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
setTrayIgnoreDoubleClickEvents,
|
|
33
35
|
setTrayPressedIcon,
|
|
34
36
|
startTrayIconAnimation,
|
|
37
|
+
stopTrayIconAnimation,
|
|
35
38
|
} from './tray';
|
|
36
39
|
|
|
37
40
|
// Fake backend state per icon.
|
|
@@ -54,6 +57,9 @@ function fakeBackend(caps: Partial<TrayCapabilities> = {}): TrayBackend & {
|
|
|
54
57
|
capabilities: TrayCapabilities;
|
|
55
58
|
fireEvent(event: TrayEventData): void;
|
|
56
59
|
lastPopupPosition: { x: number; y: number } | null;
|
|
60
|
+
// Every icon written, in order — the animation tests assert on the sequence of writes rather than
|
|
61
|
+
// the final image, because two competing animations can leave a correct-looking final frame.
|
|
62
|
+
setIconCalls: string[];
|
|
57
63
|
trays: Map<number, FakeTray>;
|
|
58
64
|
} {
|
|
59
65
|
const capabilities: TrayCapabilities = {
|
|
@@ -68,9 +74,12 @@ function fakeBackend(caps: Partial<TrayCapabilities> = {}): TrayBackend & {
|
|
|
68
74
|
let eventListener: ((event: Readonly<TrayEventData>) => void) | null = null;
|
|
69
75
|
const trays = new Map<number, FakeTray>();
|
|
70
76
|
|
|
77
|
+
const setIconCalls: string[] = [];
|
|
78
|
+
|
|
71
79
|
return {
|
|
72
80
|
capabilities,
|
|
73
81
|
trays,
|
|
82
|
+
setIconCalls,
|
|
74
83
|
lastPopupPosition: null,
|
|
75
84
|
create(options) {
|
|
76
85
|
const id = nextId++;
|
|
@@ -129,6 +138,7 @@ function fakeBackend(caps: Partial<TrayCapabilities> = {}): TrayBackend & {
|
|
|
129
138
|
if (tray) tray.contextMenu = items;
|
|
130
139
|
},
|
|
131
140
|
setIcon(id, icon) {
|
|
141
|
+
setIconCalls.push(icon);
|
|
132
142
|
const tray = trays.get(id);
|
|
133
143
|
if (tray) tray.icon = icon;
|
|
134
144
|
},
|
|
@@ -396,6 +406,43 @@ describe('isTrayDestroyed', () => {
|
|
|
396
406
|
});
|
|
397
407
|
});
|
|
398
408
|
|
|
409
|
+
describe('isTrayIconAnimating', () => {
|
|
410
|
+
beforeEach(() => {
|
|
411
|
+
vi.useFakeTimers();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
afterEach(() => {
|
|
415
|
+
vi.useRealTimers();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('is false before, true during, and false after an animation', () => {
|
|
419
|
+
setTrayBackend(fakeBackend());
|
|
420
|
+
const tray = createTrayIcon({ icon: 'a.png' })!;
|
|
421
|
+
expect(isTrayIconAnimating(tray)).toBe(false);
|
|
422
|
+
const stop = startTrayIconAnimation(tray, ['a.png', 'b.png'], 100);
|
|
423
|
+
expect(isTrayIconAnimating(tray)).toBe(true);
|
|
424
|
+
stop();
|
|
425
|
+
expect(isTrayIconAnimating(tray)).toBe(false);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('is false for an empty frame list, which starts nothing', () => {
|
|
429
|
+
setTrayBackend(fakeBackend());
|
|
430
|
+
const tray = createTrayIcon({ icon: 'a.png' })!;
|
|
431
|
+
startTrayIconAnimation(tray, [], 100);
|
|
432
|
+
expect(isTrayIconAnimating(tray)).toBe(false);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('tracks each tray independently', () => {
|
|
436
|
+
setTrayBackend(fakeBackend());
|
|
437
|
+
const first = createTrayIcon({ icon: 'a.png' })!;
|
|
438
|
+
const second = createTrayIcon({ icon: 'b.png' })!;
|
|
439
|
+
startTrayIconAnimation(first, ['a.png', 'b.png'], 100);
|
|
440
|
+
expect(isTrayIconAnimating(first)).toBe(true);
|
|
441
|
+
expect(isTrayIconAnimating(second)).toBe(false);
|
|
442
|
+
stopTrayIconAnimation(first);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
399
446
|
describe('onTrayEvent', () => {
|
|
400
447
|
it('delivers rich TrayEventData via the active backend', () => {
|
|
401
448
|
const backend = fakeBackend();
|
|
@@ -494,6 +541,42 @@ describe('removeTrayBalloon', () => {
|
|
|
494
541
|
});
|
|
495
542
|
});
|
|
496
543
|
|
|
544
|
+
describe('setTrayAnimationGuard', () => {
|
|
545
|
+
afterEach(() => {
|
|
546
|
+
setTrayAnimationGuard(null);
|
|
547
|
+
setTrayBackend(null);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// The seam keeps the message and the @flighthq/log dependency in the separately-importable guard
|
|
551
|
+
// module; startTrayIconAnimation only calls whatever is installed.
|
|
552
|
+
it('reports the frame count and interval for each animation start', () => {
|
|
553
|
+
setTrayBackend(fakeBackend());
|
|
554
|
+
const seen: Array<[number, number]> = [];
|
|
555
|
+
setTrayAnimationGuard((_tray, frameCount, intervalMs) => seen.push([frameCount, intervalMs]));
|
|
556
|
+
const tray = createTrayIcon()!;
|
|
557
|
+
|
|
558
|
+
stopTrayIconAnimation(tray);
|
|
559
|
+
startTrayIconAnimation(tray, ['a', 'b', 'c'], 0);
|
|
560
|
+
stopTrayIconAnimation(tray);
|
|
561
|
+
|
|
562
|
+
expect(seen).toEqual([[3, 0]]);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('stops reporting once cleared with null', () => {
|
|
566
|
+
setTrayBackend(fakeBackend());
|
|
567
|
+
let calls = 0;
|
|
568
|
+
setTrayAnimationGuard(() => (calls += 1));
|
|
569
|
+
const tray = createTrayIcon()!;
|
|
570
|
+
startTrayIconAnimation(tray, ['a'], 5);
|
|
571
|
+
stopTrayIconAnimation(tray);
|
|
572
|
+
setTrayAnimationGuard(null);
|
|
573
|
+
startTrayIconAnimation(tray, ['a'], 5);
|
|
574
|
+
stopTrayIconAnimation(tray);
|
|
575
|
+
|
|
576
|
+
expect(calls).toBe(1);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
497
580
|
describe('setTrayBackend', () => {
|
|
498
581
|
it('clears back to the web fallback when passed null', () => {
|
|
499
582
|
setTrayBackend(fakeBackend());
|
|
@@ -645,3 +728,104 @@ describe('startTrayIconAnimation', () => {
|
|
|
645
728
|
expect(() => stop()).not.toThrow();
|
|
646
729
|
});
|
|
647
730
|
});
|
|
731
|
+
|
|
732
|
+
describe('stopTrayIconAnimation', () => {
|
|
733
|
+
beforeEach(() => {
|
|
734
|
+
vi.useFakeTimers();
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
afterEach(() => {
|
|
738
|
+
vi.useRealTimers();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('stops the interval so no further frames are written', () => {
|
|
742
|
+
const backend = fakeBackend();
|
|
743
|
+
setTrayBackend(backend);
|
|
744
|
+
const tray = createTrayIcon({ icon: 'frame0.png' })!;
|
|
745
|
+
startTrayIconAnimation(tray, ['frame0.png', 'frame1.png'], 100);
|
|
746
|
+
stopTrayIconAnimation(tray);
|
|
747
|
+
const iconAtStop = backend.trays.get(tray.id)!.icon;
|
|
748
|
+
vi.advanceTimersByTime(500);
|
|
749
|
+
expect(backend.trays.get(tray.id)!.icon).toBe(iconAtStop);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('is idempotent and safe on a tray that never animated', () => {
|
|
753
|
+
setTrayBackend(fakeBackend());
|
|
754
|
+
const tray = createTrayIcon({ icon: 'a.png' })!;
|
|
755
|
+
expect(() => {
|
|
756
|
+
stopTrayIconAnimation(tray);
|
|
757
|
+
stopTrayIconAnimation(tray);
|
|
758
|
+
}).not.toThrow();
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
describe('tray icon animation lifecycle', () => {
|
|
763
|
+
beforeEach(() => {
|
|
764
|
+
vi.useFakeTimers();
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
afterEach(() => {
|
|
768
|
+
vi.useRealTimers();
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('replaces a running animation instead of running both', () => {
|
|
772
|
+
// The regression this pins: two starts on one tray left two intervals writing to the same icon on
|
|
773
|
+
// the same tick — the icon flickered between the two sequences, and the only handle to the first
|
|
774
|
+
// was a stop function the caller had no reason to still hold.
|
|
775
|
+
const backend = fakeBackend();
|
|
776
|
+
setTrayBackend(backend);
|
|
777
|
+
const tray = createTrayIcon({ icon: 'a0.png' })!;
|
|
778
|
+
startTrayIconAnimation(tray, ['a0.png', 'a1.png'], 100);
|
|
779
|
+
startTrayIconAnimation(tray, ['b0.png', 'b1.png'], 100);
|
|
780
|
+
backend.setIconCalls.length = 0;
|
|
781
|
+
vi.advanceTimersByTime(100);
|
|
782
|
+
expect(backend.setIconCalls).toEqual(['b1.png']);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('stops animating a tray that has been destroyed', () => {
|
|
786
|
+
// The regression this pins: the interval outlived the icon, calling setIcon on a destroyed tray
|
|
787
|
+
// forever — and the timer kept its own closure alive, so nothing could collect it.
|
|
788
|
+
const backend = fakeBackend();
|
|
789
|
+
setTrayBackend(backend);
|
|
790
|
+
const tray = createTrayIcon({ icon: 'a0.png' })!;
|
|
791
|
+
startTrayIconAnimation(tray, ['a0.png', 'a1.png'], 100);
|
|
792
|
+
destroyTrayIcon(tray);
|
|
793
|
+
backend.setIconCalls.length = 0;
|
|
794
|
+
vi.advanceTimersByTime(500);
|
|
795
|
+
expect(backend.setIconCalls).toEqual([]);
|
|
796
|
+
expect(isTrayIconAnimating(tray)).toBe(false);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('a stale stop function cannot cancel the animation that replaced it', () => {
|
|
800
|
+
const backend = fakeBackend();
|
|
801
|
+
setTrayBackend(backend);
|
|
802
|
+
const tray = createTrayIcon({ icon: 'a0.png' })!;
|
|
803
|
+
const staleStop = startTrayIconAnimation(tray, ['a0.png', 'a1.png'], 100);
|
|
804
|
+
startTrayIconAnimation(tray, ['b0.png', 'b1.png'], 100);
|
|
805
|
+
staleStop();
|
|
806
|
+
expect(isTrayIconAnimating(tray)).toBe(true);
|
|
807
|
+
backend.setIconCalls.length = 0;
|
|
808
|
+
vi.advanceTimersByTime(100);
|
|
809
|
+
expect(backend.setIconCalls).toEqual(['b1.png']);
|
|
810
|
+
stopTrayIconAnimation(tray);
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
describe('TrayIcon handle identity', () => {
|
|
815
|
+
afterEach(() => setTrayBackend(null));
|
|
816
|
+
|
|
817
|
+
// Documented contract: handles compare by id, never by identity. Pinned so the doc and the behaviour
|
|
818
|
+
// cannot drift apart, and so anyone who later interns handles has to change this deliberately rather
|
|
819
|
+
// than discover it. The working form is the id comparison, and it is asserted alongside the failing
|
|
820
|
+
// one so the test shows the remedy, not just the trap.
|
|
821
|
+
it('mints a distinct object per call, so handles compare by id rather than identity', () => {
|
|
822
|
+
const backend = fakeBackend();
|
|
823
|
+
setTrayBackend(backend);
|
|
824
|
+
const tray = createTrayIcon()!;
|
|
825
|
+
|
|
826
|
+
const listed = getTrayIcons();
|
|
827
|
+
|
|
828
|
+
expect(listed.includes(tray)).toBe(false);
|
|
829
|
+
expect(listed.some((candidate) => candidate.id === tray.id)).toBe(true);
|
|
830
|
+
});
|
|
831
|
+
});
|