@caperjs/plugin-tauri 0.0.0-stage → 0.7.2

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 CHANGED
@@ -1,3 +1,118 @@
1
- # Temporary Holding Version
1
+ # @caperjs/plugin-tauri
2
+
3
+ Native-shell integration for [Caper](https://github.com/anthonysapp/caper) games
4
+ packaged with [Tauri v2](https://v2.tauri.app/).
5
+
6
+ A Tauri app is your `dist/` running inside the system webview. That webview is
7
+ not a browser tab, and three things differ enough to break a game:
8
+
9
+ - **There is no HTML Fullscreen API.** `requestFullscreen` does not exist, so
10
+ core's `app.fullScreen` cannot work. This plugin installs a `FullscreenDriver`
11
+ that drives the real native window instead.
12
+ - **A hidden or occluded window freezes the page** — `requestAnimationFrame` and
13
+ timers simply stop. This plugin pauses the app before that happens, so it
14
+ comes back in a sane state instead of jumping.
15
+ - **`localStorage` may be evicted by WebKit.** This plugin backs `save`/`load`
16
+ with `@tauri-apps/plugin-store`, a real file on disk.
17
+
18
+ It also suppresses the webview's own right-click menu in production builds.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pnpm add @caperjs/plugin-tauri @tauri-apps/api @tauri-apps/plugin-store
24
+ ```
25
+
26
+ `@tauri-apps/api` and `@tauri-apps/plugin-store` are peer dependencies.
27
+
28
+ ## Use
29
+
30
+ ```ts
31
+ // caper.config.ts
32
+ export default defineConfig({
33
+ plugins: [
34
+ 'tauri',
35
+ // or, with options:
36
+ // ['tauri', { options: { pauseOnBlur: true, storeFile: 'slots.json' } }],
37
+ ],
38
+ });
39
+ ```
40
+
41
+ ```ts
42
+ // anywhere in the game
43
+ await app.store.save('tauri', 'progress', { level: 3 });
44
+ const progress = await app.store.load('tauri', 'progress');
45
+ ```
46
+
47
+ ## Works on the web too
48
+
49
+ Leave the plugin in `plugins: [...]` for every target. Off-native it is inert:
50
+ nothing from `@tauri-apps/*` is ever imported (all native code sits behind
51
+ `await import(...)` guarded by `isTauri`), nothing is paused, no fullscreen
52
+ driver is installed, and `quit()` is a logged no-op. Only `save`/`load` still
53
+ do something — they fall back to `localStorage`, JSON-encoded under keys
54
+ prefixed `caper:` — so a game can name this plugin as its store adapter on
55
+ desktop and on the web without branching.
56
+
57
+ ## Options
58
+
59
+ | Option | Default | What it does |
60
+ | -------------------- | ------------------------ | --------------------------------------------------------------------------------- |
61
+ | `pauseOnHide` | `true` | `app.pause()` when the webview is hidden, `app.resume()` when it comes back. |
62
+ | `pauseOnBlur` | `false` | Same, driven by the native window losing and regaining focus. |
63
+ | `nativeFullscreen` | `true` | Desktop only: install a fullscreen driver on `app.fullScreen` backed by the native window. On Android/iOS the webview's own fullscreen is kept (Tauri's `setFullscreen` is unsupported there). |
64
+ | `disableContextMenu` | `true` in prod, `false` in dev | `preventDefault()` on `contextmenu`, hiding the webview's own menu. |
65
+ | `storeFile` | `'caper-save.json'` | File under the app data dir that `save`/`load` persist to. |
66
+ | `backButtonKey` | `'GoBack'` | Android only: the key a back press is delivered as. Bind it in `controls.keyboard` like any key (`close: ['Escape', 'GoBack']`); action contexts route it. Back never closes the app while this is on. `false` keeps Android's default. |
67
+
68
+ Pausing is ownership-aware: the plugin only resumes an app it paused itself, so
69
+ a game already paused by its own menu is not resumed out from under the player.
70
+
71
+ ## What the Rust side needs
72
+
73
+ The plugin talks to Tauri over the IPC bridge, which is deny-by-default. In
74
+ `src-tauri`:
75
+
76
+ 1. Add the store plugin crate to `src-tauri/Cargo.toml`:
77
+
78
+ ```toml
79
+ [dependencies]
80
+ tauri-plugin-store = "2"
81
+ ```
82
+
83
+ and register it in `src-tauri/src/lib.rs`:
84
+
85
+ ```rust
86
+ tauri::Builder::default()
87
+ .plugin(tauri_plugin_store::Builder::new().build())
88
+ ```
89
+
90
+ 2. Grant these permissions in `src-tauri/capabilities/default.json`:
91
+
92
+ | Permission | Needed for |
93
+ | --------------------------------- | ----------------------------- |
94
+ | `store:default` | `save` / `load` |
95
+ | `core:window:allow-set-fullscreen`| entering / leaving fullscreen |
96
+ | `core:window:allow-is-fullscreen` | reading the fullscreen state |
97
+ | `core:window:allow-close` | `quit()` |
98
+
99
+ Without them the matching calls reject at runtime; the plugin surfaces the
100
+ rejection rather than swallowing it.
101
+
102
+ `caper native plugin` wires both steps up for you.
103
+
104
+ ## License
105
+
106
+ MIT
107
+
108
+ ## Android
109
+
110
+ - **Back button:** delivered as the key `GoBack` (see `backButtonKey`). Bind it in your controls
111
+ config, e.g. `toggle_pause: ['P', 'GoBack']`, `close: ['Escape', 'GoBack']`; action contexts decide
112
+ which fires. While the option is on, back never closes the app by itself.
113
+ - **Fullscreen:** Tauri's `setFullscreen()` is unsupported on mobile, so no native driver is
114
+ installed there and `app.fullScreen` keeps using the webview's own Fullscreen API. Hide the system
115
+ bars in `MainActivity.kt` instead; see the repo's `docs/wiki/native-tauri.md`.
116
+ - **Pause:** `pauseOnHide` covers the Home button and app switching (`tauri://suspended` /
117
+ `tauri://resumed` are wired too).
2
118
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
@@ -0,0 +1,116 @@
1
+ import { IApplication, IPlugin, Plugin } from '@caperjs/core';
2
+ import { version } from './version';
3
+ /** Options for {@link TauriPlugin}, all optional. */
4
+ export interface TauriPluginOptions {
5
+ /** Pause the app while the webview is hidden/occluded. Default `true`. */
6
+ pauseOnHide?: boolean;
7
+ /** Pause the app while the native window is unfocused. Default `false`. */
8
+ pauseOnBlur?: boolean;
9
+ /** Drive `app.fullScreen` through the native window. Default `true`. */
10
+ nativeFullscreen?: boolean;
11
+ /** Swallow the webview's right-click menu. Default: `true` in production, `false` in dev. */
12
+ disableContextMenu?: boolean;
13
+ /** File the disk-backed key/value store lives in, under the app data dir. Default `'caper-save.json'`. */
14
+ storeFile?: string;
15
+ /**
16
+ * Android only: the keyboard key a back-button press (or back gesture) is delivered
17
+ * as. Bind it in your controls config like any key, e.g.
18
+ * `close: ['Escape', 'GoBack']`, `toggle_pause: ['P', 'GoBack']`; action contexts
19
+ * decide which one fires. While this is on, back never closes the app by itself.
20
+ * `false` leaves Android's default behavior alone. Default `'GoBack'` (the DOM
21
+ * standard name for this key).
22
+ */
23
+ backButtonKey?: string | false;
24
+ }
25
+ /**
26
+ * Public contract for the Tauri plugin. `save`/`load` make it storage-capable, so
27
+ * it can be named as a `app.store` adapter — on native *and* on the web.
28
+ */
29
+ export interface ITauriPlugin extends IPlugin<TauriPluginOptions> {
30
+ /** Whether the app is actually running inside a Tauri webview. */
31
+ readonly isNative: boolean;
32
+ save(key: string, data: any): Promise<void>;
33
+ load<T = any>(key: string): Promise<T | undefined>;
34
+ /** Close the native window. A logged no-op on the web. */
35
+ quit(): Promise<void>;
36
+ }
37
+ /**
38
+ * Native-shell integration for apps packaged with Tauri v2.
39
+ *
40
+ * Everything native is reached through `await import(...)` behind an `isTauri`
41
+ * guard, so the same `plugins: [...]` list loads unchanged on the plain web —
42
+ * where the plugin is inert apart from `save`/`load`, which fall back to
43
+ * `localStorage`.
44
+ *
45
+ * What it does on native:
46
+ * - pauses the app when the webview is hidden (a hidden Tauri window freezes
47
+ * `requestAnimationFrame` and timers outright), and optionally on blur;
48
+ * - installs a `FullscreenDriver` on `app.fullScreen`, because Tauri's webview
49
+ * has no HTML Fullscreen API at all;
50
+ * - backs `save`/`load` with `@tauri-apps/plugin-store`, a real file on disk,
51
+ * rather than `localStorage` (which WebKit may evict);
52
+ * - suppresses the webview's own right-click menu.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * // caper.config.ts
57
+ * plugins: [['tauri', { options: { pauseOnBlur: true } }]],
58
+ *
59
+ * // anywhere
60
+ * await app.store.save('tauri', 'progress', { level: 3 });
61
+ * ```
62
+ */
63
+ export declare class TauriPlugin extends Plugin<TauriPluginOptions> implements ITauriPlugin {
64
+ readonly id = "tauri";
65
+ /** `app.fullScreen` must exist before `postInitialize` installs the driver. */
66
+ readonly requires: string[];
67
+ protected _options: Required<TauriPluginOptions>;
68
+ /** The native window handle, resolved once on native. */
69
+ private _window;
70
+ /** In-flight/settled store open, so the file is opened exactly once. */
71
+ private _storePromise;
72
+ /** Whether *this* plugin is the one that paused the app. */
73
+ private _didPause;
74
+ /** The `notify` callback handed over by the fullscreen driver's `subscribe`. */
75
+ private _notifyFullscreen;
76
+ /** So the off-native `quit()` warning is logged once, not once per call. */
77
+ private _warnedQuit;
78
+ /** Whether the app is actually running inside a Tauri webview. */
79
+ get isNative(): boolean;
80
+ initialize(options: Partial<TauriPluginOptions>, _app: IApplication): Promise<void>;
81
+ postInitialize(_app: IApplication): Promise<void>;
82
+ /**
83
+ * Persist `data` under `key` — to the disk-backed store on native, to
84
+ * `localStorage` on the web. Rejects (never swallows) on failure.
85
+ */
86
+ save(key: string, data: any): Promise<void>;
87
+ /**
88
+ * Read back what `save` wrote. A missing key resolves `undefined`; so does a
89
+ * corrupt `localStorage` entry on the web.
90
+ */
91
+ load<T = any>(key: string): Promise<T | undefined>;
92
+ /** Close the native window. A logged no-op on the web. */
93
+ quit(): Promise<void>;
94
+ /** Resolve (once) the native window handle. Only ever called when `isTauri`. */
95
+ private _nativeWindow;
96
+ /** Open (once) the disk-backed store. Only ever called when `isTauri`. */
97
+ private _store;
98
+ /**
99
+ * Mobile webviews are suspended rather than hidden, so `visibilitychange` is
100
+ * not enough on iOS/Android. These events exist from `@tauri-apps/api` v2.
101
+ */
102
+ /**
103
+ * Android's back button, delivered as an ordinary key press. Caper's keyboard
104
+ * controls read keys off the document, so the game's controls config and action
105
+ * contexts decide what back does; nothing is hard-coded here.
106
+ */
107
+ private _listenBackButton;
108
+ private _listenLifecycle;
109
+ /** Pause the app, but only if nothing else already did — and remember that we did. */
110
+ private _pause;
111
+ /** Resume the app, but only if this plugin is the one that paused it. */
112
+ private _resume;
113
+ }
114
+ export { version };
115
+ export default TauriPlugin;
116
+ //# sourceMappingURL=TauriPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TauriPlugin.d.ts","sourceRoot":"","sources":["../src/TauriPlugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,OAAO,EAMZ,MAAM,EACP,MAAM,eAAe,CAAC;AAIvB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,qDAAqD;AACrD,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,6FAA6F;IAC7F,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,0GAA0G;IAC1G,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAa,SAAQ,OAAO,CAAC,kBAAkB,CAAC;IAC/D,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACnD,0DAA0D;IAC1D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,WAAY,SAAQ,MAAM,CAAC,kBAAkB,CAAE,YAAW,YAAY;IACjF,SAAgB,EAAE,WAAW;IAC7B,+EAA+E;IAC/E,SAAgB,QAAQ,WAAkB;IAE1C,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IAEjD,yDAAyD;IACzD,OAAO,CAAC,OAAO,CAA4B;IAC3C,wEAAwE;IACxE,OAAO,CAAC,aAAa,CAAoC;IACzD,4DAA4D;IAC5D,OAAO,CAAC,SAAS,CAAS;IAC1B,gFAAgF;IAChF,OAAO,CAAC,iBAAiB,CAAkD;IAC3E,4EAA4E;IAC5E,OAAO,CAAC,WAAW,CAAS;IAE5B,kEAAkE;IAClE,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAEY,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,kBAAkB,CAAC,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCnF,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAyC9D;;;OAGG;IACU,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBxD;;;OAGG;IACU,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAqB/D,0DAA0D;IAC7C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYlC,gFAAgF;YAClE,aAAa;IAQ3B,0EAA0E;IAC1E,OAAO,CAAC,MAAM;IAcd;;;OAGG;IACH;;;;OAIG;YACW,iBAAiB;YAiBjB,gBAAgB;IAc9B,sFAAsF;IACtF,OAAO,CAAC,MAAM;IAQd,yEAAyE;IACzE,OAAO,CAAC,OAAO;CAOhB;AAOD,OAAO,EAAE,OAAO,EAAE,CAAC;AACnB,eAAe,WAAW,CAAC"}
@@ -0,0 +1,92 @@
1
+ //#region ../../node_modules/.pnpm/@tauri-apps+api@2.12.1/node_modules/@tauri-apps/api/external/tslib/tslib.es6.js
2
+ function e(e, t, n, r) {
3
+ if (n === "a" && !r) throw TypeError("Private accessor was defined without a getter");
4
+ if (typeof t == "function" ? e !== t || !r : !t.has(e)) throw TypeError("Cannot read private member from an object whose class did not declare it");
5
+ return n === "m" ? r : n === "a" ? r.call(e) : r ? r.value : t.get(e);
6
+ }
7
+ function t(e, t, n, r, i) {
8
+ if (r === "m") throw TypeError("Private method is not writable");
9
+ if (r === "a" && !i) throw TypeError("Private accessor was defined without a setter");
10
+ if (typeof t == "function" ? e !== t || !i : !t.has(e)) throw TypeError("Cannot write private member to an object whose class did not declare it");
11
+ return r === "a" ? i.call(e, n) : i ? i.value = n : t.set(e, n), n;
12
+ }
13
+ //#endregion
14
+ //#region ../../node_modules/.pnpm/@tauri-apps+api@2.12.1/node_modules/@tauri-apps/api/core.js
15
+ var n, r, i, a, o = "__TAURI_TO_IPC_KEY__";
16
+ function s(e, t = !1) {
17
+ return window.__TAURI_INTERNALS__.transformCallback(e, t);
18
+ }
19
+ var c = class {
20
+ constructor(o) {
21
+ n.set(this, void 0), r.set(this, 0), i.set(this, []), a.set(this, void 0), t(this, n, o || (() => {}), "f"), this.id = s((o) => {
22
+ let s = o.index;
23
+ if ("end" in o) {
24
+ s == e(this, r, "f") ? this.cleanupCallback() : t(this, a, s, "f");
25
+ return;
26
+ }
27
+ let c = o.message;
28
+ if (s == e(this, r, "f")) {
29
+ for (e(this, n, "f").call(this, c), t(this, r, e(this, r, "f") + 1, "f"); e(this, r, "f") in e(this, i, "f");) {
30
+ let a = e(this, i, "f")[e(this, r, "f")];
31
+ e(this, n, "f").call(this, a), delete e(this, i, "f")[e(this, r, "f")], t(this, r, e(this, r, "f") + 1, "f");
32
+ }
33
+ e(this, r, "f") === e(this, a, "f") && this.cleanupCallback();
34
+ } else e(this, i, "f")[s] = c;
35
+ });
36
+ }
37
+ cleanupCallback() {
38
+ window.__TAURI_INTERNALS__.unregisterCallback(this.id);
39
+ }
40
+ set onmessage(e) {
41
+ t(this, n, e, "f");
42
+ }
43
+ get onmessage() {
44
+ return e(this, n, "f");
45
+ }
46
+ [(n = /* @__PURE__ */ new WeakMap(), r = /* @__PURE__ */ new WeakMap(), i = /* @__PURE__ */ new WeakMap(), a = /* @__PURE__ */ new WeakMap(), o)]() {
47
+ return `__CHANNEL__:${this.id}`;
48
+ }
49
+ toJSON() {
50
+ return this[o]();
51
+ }
52
+ }, l = class {
53
+ constructor(e, t, n) {
54
+ this.plugin = e, this.event = t, this.channelId = n;
55
+ }
56
+ async unregister() {
57
+ return d(`plugin:${this.plugin}|remove_listener`, {
58
+ event: this.event,
59
+ channelId: this.channelId
60
+ });
61
+ }
62
+ };
63
+ async function u(e, t, n) {
64
+ let r = new c(n);
65
+ try {
66
+ return await d(`plugin:${e}|register_listener`, {
67
+ event: t,
68
+ handler: r
69
+ }), new l(e, t, r.id);
70
+ } catch {
71
+ return await d(`plugin:${e}|registerListener`, {
72
+ event: t,
73
+ handler: r
74
+ }), new l(e, t, r.id);
75
+ }
76
+ }
77
+ async function d(e, t = {}, n) {
78
+ return window.__TAURI_INTERNALS__.invoke(e, t, n);
79
+ }
80
+ //#endregion
81
+ //#region ../../node_modules/.pnpm/@tauri-apps+api@2.12.1/node_modules/@tauri-apps/api/app.js
82
+ var f;
83
+ (function(e) {
84
+ e.Nsis = "nsis", e.Msi = "msi", e.Deb = "deb", e.Rpm = "rpm", e.AppImage = "appimage", e.App = "app";
85
+ })(f ||= {});
86
+ async function p(e) {
87
+ return u("app", "back-button", e);
88
+ }
89
+ //#endregion
90
+ export { p as onBackButtonPress };
91
+
92
+ //# sourceMappingURL=app-BYxGaqQR.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-BYxGaqQR.mjs","names":[],"sources":["../../../node_modules/.pnpm/@tauri-apps+api@2.12.1/node_modules/@tauri-apps/api/external/tslib/tslib.es6.js","../../../node_modules/.pnpm/@tauri-apps+api@2.12.1/node_modules/@tauri-apps/api/core.js","../../../node_modules/.pnpm/@tauri-apps+api@2.12.1/node_modules/@tauri-apps/api/app.js"],"sourcesContent":["/******************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\r\n\r\n\r\nfunction __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nfunction __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n\r\ntypeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\r\n var e = new Error(message);\r\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\r\n};\n\nexport { __classPrivateFieldGet, __classPrivateFieldSet };\n","import { __classPrivateFieldSet, __classPrivateFieldGet } from './external/tslib/tslib.es6.js';\n\n// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\nvar _Channel_onmessage, _Channel_nextMessageIndex, _Channel_pendingMessages, _Channel_messageEndIndex, _Resource_rid;\n/**\n * Invoke your custom commands.\n *\n * This package is also accessible with `window.__TAURI__.core` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.\n * @module\n */\n/**\n * A key to be used to implement a special function\n * on your types that define how your type should be serialized\n * when passing across the IPC.\n * @example\n * Given a type in Rust that looks like this\n * ```rs\n * #[derive(serde::Serialize, serde::Deserialize)\n * enum UserId {\n * String(String),\n * Number(u32),\n * }\n * ```\n * `UserId::String(\"id\")` would be serialized into `{ String: \"id\" }`\n * and so we need to pass the same structure back to Rust\n * ```ts\n * import { SERIALIZE_TO_IPC_FN } from \"@tauri-apps/api/core\"\n *\n * class UserIdString {\n * id\n * constructor(id) {\n * this.id = id\n * }\n *\n * [SERIALIZE_TO_IPC_FN]() {\n * return { String: this.id }\n * }\n * }\n *\n * class UserIdNumber {\n * id\n * constructor(id) {\n * this.id = id\n * }\n *\n * [SERIALIZE_TO_IPC_FN]() {\n * return { Number: this.id }\n * }\n * }\n *\n * type UserId = UserIdString | UserIdNumber\n * ```\n *\n */\n// if this value changes, make sure to update it in:\n// 1. ipc.js\n// 2. process-ipc-message-fn.js\nconst SERIALIZE_TO_IPC_FN = '__TAURI_TO_IPC_KEY__';\n/**\n * Stores the callback in a known location, and returns an identifier that can be passed to the backend.\n * The backend uses the identifier to `eval()` the callback.\n *\n * @return An unique identifier associated with the callback function.\n *\n * @since 1.0.0\n */\nfunction transformCallback(\n// TODO: Make this not optional in v3\ncallback, once = false) {\n return window.__TAURI_INTERNALS__.transformCallback(callback, once);\n}\n/**\n * A message channel used to stream values from Rust to the frontend.\n *\n * A `Channel` is the JavaScript counterpart of [`tauri::ipc::Channel`](https://docs.rs/tauri/2/tauri/ipc/struct.Channel.html).\n * Pass an instance as a command argument and the Rust command can declare a\n * `tauri::ipc::Channel<T>` parameter with the same name; every value sent from\n * Rust is then delivered to {@linkcode Channel.onmessage}. Messages are delivered\n * in the order they were sent, even when they arrive out of order.\n *\n * The channel stays alive until the Rust `Channel` is dropped, so it can be used\n * for long-running work such as download progress or log streaming.\n *\n * Raw byte payloads are delivered to the frontend as an `ArrayBuffer` rather than\n * a JSON value, so declare the channel as `Channel<ArrayBuffer>` when the Rust\n * side sends `tauri::ipc::InvokeResponseBody::Raw` — for example through a\n * `Channel<tauri::ipc::Response>` whose values are built with\n * `tauri::ipc::Response::new(bytes)`.\n *\n * @example\n * ```typescript\n * import { Channel, invoke } from '@tauri-apps/api/core';\n *\n * const onEvent = new Channel<string>();\n * onEvent.onmessage = (message) => {\n * console.log(`got download event ${message}`);\n * };\n *\n * await invoke('download', { url: 'https://tauri.app', onEvent });\n * ```\n *\n * The matching Rust command:\n * ```rust\n * #[tauri::command]\n * async fn download(url: String, on_event: tauri::ipc::Channel<String>) -> tauri::Result<()> {\n * on_event.send(\"started\".to_string())?;\n * on_event.send(\"finished\".to_string())?;\n * Ok(())\n * }\n * ```\n *\n * @since 2.0.0\n */\nclass Channel {\n constructor(onmessage) {\n _Channel_onmessage.set(this, void 0);\n // the index is used as a mechanism to preserve message order\n _Channel_nextMessageIndex.set(this, 0);\n _Channel_pendingMessages.set(this, []);\n _Channel_messageEndIndex.set(this, void 0);\n __classPrivateFieldSet(this, _Channel_onmessage, onmessage || (() => { }), \"f\");\n this.id = transformCallback((rawMessage) => {\n const index = rawMessage.index;\n if ('end' in rawMessage) {\n if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, \"f\")) {\n this.cleanupCallback();\n }\n else {\n __classPrivateFieldSet(this, _Channel_messageEndIndex, index, \"f\");\n }\n return;\n }\n const message = rawMessage.message;\n // Process the message if we're at the right order\n if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, \"f\")) {\n __classPrivateFieldGet(this, _Channel_onmessage, \"f\").call(this, message);\n __classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, \"f\") + 1, \"f\");\n // process pending messages\n while (__classPrivateFieldGet(this, _Channel_nextMessageIndex, \"f\") in __classPrivateFieldGet(this, _Channel_pendingMessages, \"f\")) {\n const message = __classPrivateFieldGet(this, _Channel_pendingMessages, \"f\")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, \"f\")];\n __classPrivateFieldGet(this, _Channel_onmessage, \"f\").call(this, message);\n // eslint-disable-next-line @typescript-eslint/no-array-delete\n delete __classPrivateFieldGet(this, _Channel_pendingMessages, \"f\")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, \"f\")];\n __classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, \"f\") + 1, \"f\");\n }\n if (__classPrivateFieldGet(this, _Channel_nextMessageIndex, \"f\") === __classPrivateFieldGet(this, _Channel_messageEndIndex, \"f\")) {\n this.cleanupCallback();\n }\n }\n // Queue the message if we're not\n else {\n // eslint-disable-next-line security/detect-object-injection\n __classPrivateFieldGet(this, _Channel_pendingMessages, \"f\")[index] = message;\n }\n });\n }\n cleanupCallback() {\n window.__TAURI_INTERNALS__.unregisterCallback(this.id);\n }\n /**\n * The handler called for every message sent by the Rust side of this channel.\n *\n * Assigning a new handler replaces the previous one; messages that arrived\n * before a handler was set are not replayed, so set it (or pass it to the\n * constructor) before sending the channel to the backend.\n */\n set onmessage(handler) {\n __classPrivateFieldSet(this, _Channel_onmessage, handler, \"f\");\n }\n get onmessage() {\n return __classPrivateFieldGet(this, _Channel_onmessage, \"f\");\n }\n [(_Channel_onmessage = new WeakMap(), _Channel_nextMessageIndex = new WeakMap(), _Channel_pendingMessages = new WeakMap(), _Channel_messageEndIndex = new WeakMap(), SERIALIZE_TO_IPC_FN)]() {\n return `__CHANNEL__:${this.id}`;\n }\n toJSON() {\n // eslint-disable-next-line security/detect-object-injection\n return this[SERIALIZE_TO_IPC_FN]();\n }\n}\n/**\n * A handle to a listener registered with {@linkcode addPluginListener}.\n *\n * Keep the returned instance around and call {@linkcode PluginListener.unregister}\n * when the listener goes out of scope, otherwise the plugin keeps sending events\n * to a handler nothing uses anymore.\n *\n * @since 2.0.0\n */\nclass PluginListener {\n constructor(plugin, event, channelId) {\n this.plugin = plugin;\n this.event = event;\n this.channelId = channelId;\n }\n /** Removes this listener from the plugin, so its handler stops being called. */\n async unregister() {\n return invoke(`plugin:${this.plugin}|remove_listener`, {\n event: this.event,\n channelId: this.channelId\n });\n }\n}\n/**\n * Adds a listener to a plugin event.\n *\n * This is meant to be used by plugin authors to wrap the `register_listener`\n * command their mobile plugin implements; application code normally calls the\n * wrapper the plugin exposes instead of this function.\n *\n * @example\n * ```typescript\n * import { addPluginListener } from '@tauri-apps/api/core';\n *\n * interface ScanEvent {\n * value: string\n * }\n *\n * const listener = await addPluginListener<ScanEvent>(\n * 'barcode-scanner',\n * 'scan',\n * (payload) => console.log('scanned', payload.value)\n * );\n *\n * // stop listening when the scanner screen is closed\n * await listener.unregister();\n * ```\n *\n * @param plugin The plugin name, as used in the `plugin:<name>|<command>` IPC command format.\n * @param event The plugin event name.\n * @param cb The callback executed for each event payload.\n * @returns The listener object to stop listening to the events.\n *\n * @since 2.0.0\n */\nasync function addPluginListener(plugin, event, cb) {\n const handler = new Channel(cb);\n try {\n await invoke(`plugin:${plugin}|register_listener`, {\n event,\n handler\n });\n return new PluginListener(plugin, event, handler.id);\n }\n catch {\n // TODO(v3): remove this fallback\n // note: we must try with camelCase here for backwards compatibility\n await invoke(`plugin:${plugin}|registerListener`, { event, handler });\n return new PluginListener(plugin, event, handler.id);\n }\n}\n/**\n * Get permission state for a plugin.\n *\n * This should be used by plugin authors to wrap their actual implementation,\n * it calls the `plugin:<name>|check_permissions` command implemented by the\n * mobile plugin and returns its permission status object without prompting\n * the user.\n *\n * @example\n * ```typescript\n * import { checkPermissions, type PermissionState } from '@tauri-apps/api/core';\n *\n * interface Permissions {\n * camera: PermissionState\n * }\n *\n * const status = await checkPermissions<Permissions>('barcode-scanner');\n * if (status.camera === 'prompt') {\n * // ask the user, see requestPermissions\n * }\n * ```\n *\n * @param plugin The plugin name, as used in the `plugin:<name>|<command>` IPC command format.\n *\n * @since 2.0.0\n */\nasync function checkPermissions(plugin) {\n return invoke(`plugin:${plugin}|check_permissions`);\n}\n/**\n * Request permissions.\n *\n * This should be used by plugin authors to wrap their actual implementation,\n * it calls the `plugin:<name>|request_permissions` command implemented by the\n * mobile plugin, which shows the native permission prompt when needed, and\n * returns the resulting permission status object.\n *\n * @example\n * ```typescript\n * import { requestPermissions, type PermissionState } from '@tauri-apps/api/core';\n *\n * interface Permissions {\n * camera: PermissionState\n * }\n *\n * const status = await requestPermissions<Permissions>('barcode-scanner');\n * if (status.camera !== 'granted') {\n * throw new Error('camera permission denied');\n * }\n * ```\n *\n * @param plugin The plugin name, as used in the `plugin:<name>|<command>` IPC command format.\n *\n * @since 2.0.0\n */\nasync function requestPermissions(plugin) {\n return invoke(`plugin:${plugin}|request_permissions`);\n}\n/**\n * Sends a message to the backend.\n * @example\n * ```typescript\n * import { invoke } from '@tauri-apps/api/core';\n * await invoke('login', { user: 'tauri', password: 'poiwe3h4r5ip3yrhtew9ty' });\n * ```\n *\n * @param cmd The command name.\n * @param args The optional arguments to pass to the command.\n * @param options The request options.\n * @return A promise resolving or rejecting to the backend response.\n *\n * @since 1.0.0\n */\nasync function invoke(cmd, args = {}, options) {\n return window.__TAURI_INTERNALS__.invoke(cmd, args, options);\n}\n/**\n * Convert a device file path to a URL that can be loaded by the webview.\n *\n * The asset protocol must be enabled and the files you want to expose must be\n * included in its scope. The protocol origins must also be allowed by the\n * relevant [`app.security.csp`](https://v2.tauri.app/reference/config/#csp-1)\n * directive. For example, this configuration allows images from the user's\n * downloads directory:\n *\n * ```json\n * {\n * \"app\": {\n * \"security\": {\n * \"assetProtocol\": {\n * \"enable\": true,\n * \"scope\": [\"$DOWNLOAD/**\"]\n * },\n * \"csp\": \"default-src 'self'; img-src 'self' asset: http://asset.localhost\"\n * }\n * }\n * }\n * ```\n *\n * See [`assetProtocol`](https://v2.tauri.app/reference/config/#assetprotocolconfig)\n * for the available scope variables and platform-specific protocol details.\n *\n * @param filePath The file path.\n * @param protocol The protocol to use. Defaults to `asset`. You only need to set this when using a custom protocol.\n * @example\n * ```typescript\n * import { downloadDir, join } from '@tauri-apps/api/path';\n * import { convertFileSrc } from '@tauri-apps/api/core';\n * const downloads = await downloadDir();\n * const filePath = await join(downloads, 'photo.png');\n * const assetUrl = convertFileSrc(filePath);\n *\n * const image = document.getElementById('my-image') as HTMLImageElement;\n * image.src = assetUrl;\n * ```\n *\n * @return the URL that can be used as source on the webview.\n *\n * @since 1.0.0\n */\nfunction convertFileSrc(filePath, protocol = 'asset') {\n return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol);\n}\n/**\n * A rust-backed resource stored through `tauri::Manager::resources_table` API.\n *\n * The resource lives in the main process and does not exist\n * in the Javascript world, and thus will not be cleaned up automatically\n * except on application exit. If you want to clean it up early, call {@linkcode Resource.close} or use [Explicit Resource Management].\n *\n * Several API types are resources and inherit this behavior, among them `Menu`,\n * `TrayIcon`, `Image`, `Webview` and the menu item classes.\n *\n * To support older browsers with [Explicit Resource Management], use a supported compiler (e.g. tsc) or bundler (e.g. rollup).\n *\n * @example\n * ```typescript\n * import { Resource, invoke } from '@tauri-apps/api/core';\n *\n * export class DatabaseHandle extends Resource {\n * static async open(path: string): Promise<DatabaseHandle> {\n * const rid: number = await invoke('open_db', { path });\n * return new DatabaseHandle(rid);\n * }\n *\n * async execute(sql: string): Promise<void> {\n * await invoke('execute_sql', { rid: this.rid, sql });\n * }\n * }\n * ```\n *\n * Only asynchronous disposal is implemented (`Symbol.asyncDispose`), because closing\n * a resource is an IPC call. Use `await using`; the synchronous `using` form does\n * **not** work with resources.\n *\n * @example\n * ```\n * await using db = await DatabaseHandle.open('test.db');\n * await db.execute('SELECT *');\n * // `db` is closed here, by awaiting `db[Symbol.asyncDispose]()`\n * ```\n *\n * To support older browsers, add the following to the globals (e.g. adding to the HTML file):\n *\n * ```javascript\n * Symbol.asyncDispose ??= Symbol(\"Symbol.asyncDispose\");\n * ```\n *\n * And for the compiler, for example `tsc`, `rollup`, `vite`, add the following to `tsconfig.json`:\n *\n * ```json\n * {\n * \"compilerOptions\": {\n * \"target\": \"es2022\",\n * \"lib\": [\"es2022\", \"esnext.disposable\", \"dom\"]\n * }\n * }\n * ```\n *\n * [Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management\n */\nclass Resource {\n get rid() {\n return __classPrivateFieldGet(this, _Resource_rid, \"f\");\n }\n constructor(rid) {\n _Resource_rid.set(this, void 0);\n __classPrivateFieldSet(this, _Resource_rid, rid, \"f\");\n }\n /**\n * Destroys and cleans up this resource from memory.\n * **You should not call any method on this object anymore and should drop any reference to it.**\n *\n * @remarks Uses the `core:resources:allow-close` permission, which is part of\n * the `core:resources:default` permission set enabled by default.\n */\n async close() {\n return invoke('plugin:resources|close', {\n rid: this.rid\n });\n }\n async [(_Resource_rid = new WeakMap(), Symbol.asyncDispose)]() {\n await this.close();\n }\n}\n/**\n * Checks whether the code is running inside a Tauri webview.\n *\n * Useful for frontends that are also served on the web, to guard calls to APIs\n * that only exist inside the application.\n *\n * @example\n * ```typescript\n * import { isTauri } from '@tauri-apps/api/core';\n * import { getVersion } from '@tauri-apps/api/app';\n *\n * const version = isTauri() ? await getVersion() : 'web';\n * ```\n *\n * @returns `true` when running inside a Tauri app, `false` otherwise (e.g. in a\n * browser or in unit tests).\n *\n * @since 2.0.0\n */\nfunction isTauri() {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access\n return !!(globalThis || window).isTauri;\n}\n\nexport { Channel, PluginListener, Resource, SERIALIZE_TO_IPC_FN, addPluginListener, checkPermissions, convertFileSrc, invoke, isTauri, requestPermissions, transformCallback };\n","import { invoke, addPluginListener } from './core.js';\nimport { Image } from './image.js';\n\n// Copyright 2019-2024 Tauri Programme within The Commons Conservancy\n// SPDX-License-Identifier: Apache-2.0\n// SPDX-License-Identifier: MIT\n/**\n * Application metadata and lifecycle APIs: version and identifier, theme and dock\n * visibility, data stores and exiting the app.\n *\n * This package is also accessible with `window.__TAURI__.app` when [`app.withGlobalTauri`](https://v2.tauri.app/reference/config/#withglobaltauri) in `tauri.conf.json` is set to `true`.\n *\n * @remarks Only the read-only commands are enabled by `core:app:default`\n * (`allow-version`, `allow-name`, `allow-tauri-version`, `allow-identifier`,\n * `allow-bundle-type`, `allow-supports-multiple-windows`, `allow-register-listener`\n * and `allow-remove-listener`). Every other function in this module documents the\n * permission it needs, which you must add to a capability yourself.\n *\n * @module\n */\n/**\n * Bundle type of the current application.\n *\n * @see {@linkcode getBundleType}\n *\n * @since 2.7.0\n */\nvar BundleType;\n(function (BundleType) {\n /** Windows NSIS */\n BundleType[\"Nsis\"] = \"nsis\";\n /** Windows MSI */\n BundleType[\"Msi\"] = \"msi\";\n /** Linux Debian package */\n BundleType[\"Deb\"] = \"deb\";\n /** Linux RPM */\n BundleType[\"Rpm\"] = \"rpm\";\n /** Linux AppImage */\n BundleType[\"AppImage\"] = \"appimage\";\n /** macOS app bundle */\n BundleType[\"App\"] = \"app\";\n})(BundleType || (BundleType = {}));\n/**\n * Gets the application version.\n * @example\n * ```typescript\n * import { getVersion } from '@tauri-apps/api/app';\n * const appVersion = await getVersion();\n * ```\n *\n * @since 1.0.0\n */\nasync function getVersion() {\n return invoke('plugin:app|version');\n}\n/**\n * Gets the application name.\n * @example\n * ```typescript\n * import { getName } from '@tauri-apps/api/app';\n * const appName = await getName();\n * ```\n *\n * @since 1.0.0\n */\nasync function getName() {\n return invoke('plugin:app|name');\n}\n/**\n * Gets the Tauri framework version used by this application.\n *\n * @example\n * ```typescript\n * import { getTauriVersion } from '@tauri-apps/api/app';\n * const tauriVersion = await getTauriVersion();\n * ```\n *\n * @since 1.0.0\n */\nasync function getTauriVersion() {\n return invoke('plugin:app|tauri_version');\n}\n/**\n * Gets the application identifier.\n * @example\n * ```typescript\n * import { getIdentifier } from '@tauri-apps/api/app';\n * const identifier = await getIdentifier();\n * ```\n *\n * @returns The application identifier as configured in `tauri.conf.json`.\n *\n * @since 2.4.0\n */\nasync function getIdentifier() {\n return invoke('plugin:app|identifier');\n}\n/**\n * Shows the application on macOS. This function does not automatically\n * focus any specific app window.\n *\n * @example\n * ```typescript\n * import { show } from '@tauri-apps/api/app';\n * await show();\n * ```\n *\n * @remarks Requires the `core:app:allow-app-show` permission (not included in `core:app:default`).\n *\n * @since 1.2.0\n */\nasync function show() {\n return invoke('plugin:app|app_show');\n}\n/**\n * Hides the application on macOS.\n *\n * @example\n * ```typescript\n * import { hide } from '@tauri-apps/api/app';\n * await hide();\n * ```\n *\n * @remarks Requires the `core:app:allow-app-hide` permission (not included in `core:app:default`).\n *\n * @since 1.2.0\n */\nasync function hide() {\n return invoke('plugin:app|app_hide');\n}\n/**\n * Fetches the data store identifiers on macOS and iOS.\n *\n * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information.\n *\n * @example\n * ```typescript\n * import { fetchDataStoreIdentifiers } from '@tauri-apps/api/app';\n * const ids = await fetchDataStoreIdentifiers();\n * ```\n *\n * @remarks Requires the `core:app:allow-fetch-data-store-identifiers` permission\n * (not included in `core:app:default`).\n *\n * @since 2.4.0\n */\nasync function fetchDataStoreIdentifiers() {\n return invoke('plugin:app|fetch_data_store_identifiers');\n}\n/**\n * Removes the data store with the given identifier.\n *\n * Note that any webview using this data store should be closed before running this API.\n *\n * See https://developer.apple.com/documentation/webkit/wkwebsitedatastore for more information.\n *\n * @example\n * ```typescript\n * import { fetchDataStoreIdentifiers, removeDataStore } from '@tauri-apps/api/app';\n * for (const id of (await fetchDataStoreIdentifiers())) {\n * await removeDataStore(id);\n * }\n * ```\n *\n * @remarks Requires the `core:app:allow-remove-data-store` permission (not included\n * in `core:app:default`).\n *\n * @since 2.4.0\n */\nasync function removeDataStore(uuid) {\n return invoke('plugin:app|remove_data_store', { uuid });\n}\n/**\n * Gets the default window icon.\n *\n * @example\n * ```typescript\n * import { defaultWindowIcon } from '@tauri-apps/api/app';\n * const icon = await defaultWindowIcon();\n * ```\n *\n * @remarks Requires the `core:app:allow-default-window-icon` permission (not\n * included in `core:app:default`).\n *\n * @since 2.0.0\n */\nasync function defaultWindowIcon() {\n return invoke('plugin:app|default_window_icon').then((rid) => rid ? new Image(rid) : null);\n}\n/**\n * Sets the application's theme. Pass in `null` or `undefined` to follow\n * the system theme.\n *\n * @example\n * ```typescript\n * import { setTheme } from '@tauri-apps/api/app';\n * await setTheme('dark');\n * ```\n *\n * #### Platform-specific\n *\n * - **iOS / Android:** Unsupported.\n *\n * @remarks Requires the `core:app:allow-set-app-theme` permission (not included in\n * `core:app:default`).\n *\n * @since 2.0.0\n */\nasync function setTheme(theme) {\n return invoke('plugin:app|set_app_theme', { theme });\n}\n/**\n * Sets the dock visibility for the application on macOS.\n *\n * @param visible - Whether the dock should be visible or not.\n *\n * @example\n * ```typescript\n * import { setDockVisibility } from '@tauri-apps/api/app';\n * await setDockVisibility(false);\n * ```\n *\n * @remarks Requires the `core:app:allow-set-dock-visibility` permission (not\n * included in `core:app:default`).\n *\n * @since 2.5.0\n */\nasync function setDockVisibility(visible) {\n return invoke('plugin:app|set_dock_visibility', { visible });\n}\n/**\n * Gets the application bundle type.\n *\n * @example\n * ```typescript\n * import { getBundleType } from '@tauri-apps/api/app';\n * const type = await getBundleType();\n * ```\n *\n * @since 2.5.0\n */\nasync function getBundleType() {\n return invoke('plugin:app|bundle_type');\n}\n/**\n * Listens to the Android hardware/gesture back button.\n *\n * Registering a handler takes over the default behavior, so the app no longer\n * navigates back or closes on its own: decide what to do inside the handler,\n * using `payload.canGoBack` to know whether the webview has history to go back to.\n *\n * #### Platform-specific\n *\n * - **Android:** Supported.\n * - **Windows / Linux / macOS / iOS:** Unsupported, the handler is never called.\n *\n * @example\n * ```typescript\n * import { onBackButtonPress } from '@tauri-apps/api/app';\n * import { exit } from '@tauri-apps/api/app';\n *\n * const listener = await onBackButtonPress(({ canGoBack }) => {\n * if (canGoBack) {\n * window.history.back();\n * } else {\n * void exit(0);\n * }\n * });\n *\n * // stop handling the back button\n * await listener.unregister();\n * ```\n *\n * @param handler Called on every back button press.\n * @returns A listener handle, call `unregister()` on it to restore the default behavior.\n *\n * @since 2.9.0\n */\nasync function onBackButtonPress(handler) {\n return addPluginListener('app', 'back-button', handler);\n}\n/**\n * Whether the current platform can show more than one window at a time.\n *\n * Use it to hide or disable multi-window features on platforms where creating a\n * second window is not possible.\n *\n * #### Platform-specific\n *\n * - **Windows / Linux / macOS:** Always `true`.\n * - **Android:** `true` on API level 32 (Android 12L) and above.\n * - **iOS:** Reflects [`UIApplication.supportsMultipleScenes`](https://developer.apple.com/documentation/uikit/uiapplication/supportsmultiplescenes),\n * so it is `true` on iPadOS and `false` on iPhone.\n *\n * See the [mobile multiwindow guide](https://tauri.app/learn/mobile-multiwindow/)\n * for how windows behave on mobile.\n *\n * @example\n * ```typescript\n * import { supportsMultipleWindows } from '@tauri-apps/api/app';\n * import { WebviewWindow } from '@tauri-apps/api/webviewWindow';\n *\n * if (await supportsMultipleWindows()) {\n * new WebviewWindow('settings', { url: '/settings' });\n * }\n * ```\n *\n * @since 2.11.0\n */\nasync function supportsMultipleWindows() {\n return invoke('plugin:app|supports_multiple_windows');\n}\n/**\n * Exits the app with the given exit code.\n *\n * This is the same as the `exit` function of the `@tauri-apps/plugin-process` plugin,\n * but does not require a plugin to be installed.\n *\n * #### Platform-specific\n *\n * - **Android**: The activity is finished instead of the process being killed,\n * so the app closes with the system transition; `code` is ignored.\n *\n * @example\n * ```typescript\n * import { exit } from '@tauri-apps/api/app';\n * await exit(1);\n * ```\n *\n * @param code The exit code to use. Defaults to `0`.\n * @returns A promise indicating the success or failure of the operation.\n *\n * @remarks Requires the `core:app:allow-exit` permission (not included in\n * `core:app:default`).\n *\n * @since 2.12.0\n */\nasync function exit(code = 0) {\n return invoke('plugin:app|exit', { code });\n}\n\nexport { BundleType, defaultWindowIcon, exit, fetchDataStoreIdentifiers, getBundleType, getIdentifier, getName, getTauriVersion, getVersion, hide, onBackButtonPress, removeDataStore, setDockVisibility, setTheme, show, supportsMultipleWindows };\n"],"x_google_ignoreList":[0,1,2],"mappings":"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"}
@@ -0,0 +1,145 @@
1
+ import { Logger as e, Plugin as t, isAndroid as n, isDev as r, isMobile as i, isTauri as a } from "@caperjs/core";
2
+ //#region src/version.ts
3
+ var o = "0.7.2", s = "caper:";
4
+ function c() {
5
+ return {
6
+ pauseOnHide: !0,
7
+ pauseOnBlur: !1,
8
+ nativeFullscreen: !0,
9
+ disableContextMenu: !r,
10
+ storeFile: "caper-save.json",
11
+ backButtonKey: "GoBack"
12
+ };
13
+ }
14
+ var l = class extends t {
15
+ constructor(...e) {
16
+ super(...e), this.id = "tauri", this.requires = ["fullscreen"], this._window = null, this._storePromise = null, this._didPause = !1, this._notifyFullscreen = null, this._warnedQuit = !1;
17
+ }
18
+ get isNative() {
19
+ return a;
20
+ }
21
+ async initialize(e, t) {
22
+ if (this._options = {
23
+ ...c(),
24
+ ...e
25
+ }, !a) return;
26
+ this._options.disableContextMenu && this.listen(document, "contextmenu", (e) => e.preventDefault()), this._options.pauseOnHide && this.listen(document, "visibilitychange", () => {
27
+ document.visibilityState === "hidden" ? this._pause() : this._resume();
28
+ });
29
+ let n = await this._nativeWindow();
30
+ this._options.pauseOnBlur && this.addDisposer(await n.onFocusChanged(({ payload: e }) => {
31
+ e ? this._resume() : this._pause();
32
+ })), await this._listenLifecycle(), await this._listenBackButton();
33
+ }
34
+ async postInitialize(t) {
35
+ if (!a || i || !this._options.nativeFullscreen) return;
36
+ let n = this.app.fullScreen, r = await this._nativeWindow();
37
+ this.addDisposer(await r.onResized(async () => {
38
+ if (this._notifyFullscreen) try {
39
+ this._notifyFullscreen(await r.isFullscreen());
40
+ } catch (t) {
41
+ e.error(`[${this.id}] could not read the native fullscreen state:`, t);
42
+ }
43
+ })), n.setFullscreenDriver({
44
+ supported: !0,
45
+ request: () => r.setFullscreen(!0),
46
+ exit: () => r.setFullscreen(!1),
47
+ subscribe: (e) => (this._notifyFullscreen = e, () => {
48
+ this._notifyFullscreen = null;
49
+ })
50
+ }), this.addDisposer(() => n.setFullscreenDriver(null));
51
+ }
52
+ async save(e, t) {
53
+ if (!a) {
54
+ try {
55
+ localStorage.setItem(`${s}${e}`, JSON.stringify(t));
56
+ } catch (t) {
57
+ throw Error(`[${this.id}] failed to save "${e}" to localStorage: ${u(t)}`);
58
+ }
59
+ return;
60
+ }
61
+ try {
62
+ let n = await this._store();
63
+ await n.set(e, t), await n.save();
64
+ } catch (t) {
65
+ throw Error(`[${this.id}] failed to save "${e}" to ${this._options.storeFile}: ${u(t)}`);
66
+ }
67
+ }
68
+ async load(e) {
69
+ if (!a) {
70
+ let t = localStorage.getItem(`${s}${e}`);
71
+ if (t === null) return;
72
+ try {
73
+ return JSON.parse(t);
74
+ } catch {
75
+ return;
76
+ }
77
+ }
78
+ try {
79
+ return await (await this._store()).get(e);
80
+ } catch (t) {
81
+ throw Error(`[${this.id}] failed to load "${e}" from ${this._options.storeFile}: ${u(t)}`);
82
+ }
83
+ }
84
+ async quit() {
85
+ if (!a) {
86
+ this._warnedQuit || (this._warnedQuit = !0, e.warn(`[${this.id}] quit() does nothing outside a Tauri window`));
87
+ return;
88
+ }
89
+ await (await this._nativeWindow()).close();
90
+ }
91
+ async _nativeWindow() {
92
+ if (!this._window) {
93
+ let { getCurrentWindow: e } = await import("@tauri-apps/api/window");
94
+ this._window = e();
95
+ }
96
+ return this._window;
97
+ }
98
+ _store() {
99
+ return this._storePromise || (this._storePromise = (async () => {
100
+ let { load: e } = await import("@tauri-apps/plugin-store");
101
+ return e(this._options.storeFile, { autoSave: !0 });
102
+ })(), this._storePromise.catch(() => {
103
+ this._storePromise = null;
104
+ })), this._storePromise;
105
+ }
106
+ async _listenBackButton() {
107
+ let t = this._options.backButtonKey;
108
+ if (!(!n || !t)) try {
109
+ let { onBackButtonPress: e } = await import("./app-BYxGaqQR.mjs"), n = await e(() => {
110
+ document.dispatchEvent(new KeyboardEvent("keydown", {
111
+ key: t,
112
+ bubbles: !0
113
+ })), document.dispatchEvent(new KeyboardEvent("keyup", {
114
+ key: t,
115
+ bubbles: !0
116
+ }));
117
+ });
118
+ this.addDisposer(() => void n.unregister());
119
+ } catch (t) {
120
+ e.error(`[${this.id}] could not listen for the Android back button:`, t);
121
+ }
122
+ }
123
+ async _listenLifecycle() {
124
+ let { listen: e, TauriEvent: t } = await import("@tauri-apps/api/event"), n = t.WINDOW_SUSPENDED, r = t.WINDOW_RESUMED;
125
+ if (!n || !r) return;
126
+ let i = await Promise.all([e(n, () => this._pause()), e(r, () => this._resume())]);
127
+ this.addDisposer(...i);
128
+ }
129
+ _pause() {
130
+ this.app.paused || (this.app.pause(), this._didPause = !0);
131
+ }
132
+ _resume() {
133
+ this._didPause && (this._didPause = !1, this.app.resume());
134
+ }
135
+ };
136
+ function u(e) {
137
+ return e instanceof Error ? e.message : String(e);
138
+ }
139
+ //#endregion
140
+ //#region src/index.ts
141
+ var d = l;
142
+ //#endregion
143
+ export { l as TauriPlugin, d as default, o as version };
144
+
145
+ //# sourceMappingURL=caper-plugin-tauri.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caper-plugin-tauri.mjs","names":[],"sources":["../src/version.ts","../src/TauriPlugin.ts","../src/index.ts"],"sourcesContent":["export const version = '0.7.2';\n","import {\n type FullscreenDriver,\n type IApplication,\n type IPlugin,\n isAndroid,\n isDev,\n isMobile,\n isTauri,\n Logger,\n Plugin,\n} from '@caperjs/core';\nimport type { UnlistenFn } from '@tauri-apps/api/event';\nimport type { Window as TauriWindow } from '@tauri-apps/api/window';\nimport type { Store as TauriStore } from '@tauri-apps/plugin-store';\nimport { version } from './version';\n\n/** Options for {@link TauriPlugin}, all optional. */\nexport interface TauriPluginOptions {\n /** Pause the app while the webview is hidden/occluded. Default `true`. */\n pauseOnHide?: boolean;\n /** Pause the app while the native window is unfocused. Default `false`. */\n pauseOnBlur?: boolean;\n /** Drive `app.fullScreen` through the native window. Default `true`. */\n nativeFullscreen?: boolean;\n /** Swallow the webview's right-click menu. Default: `true` in production, `false` in dev. */\n disableContextMenu?: boolean;\n /** File the disk-backed key/value store lives in, under the app data dir. Default `'caper-save.json'`. */\n storeFile?: string;\n /**\n * Android only: the keyboard key a back-button press (or back gesture) is delivered\n * as. Bind it in your controls config like any key, e.g.\n * `close: ['Escape', 'GoBack']`, `toggle_pause: ['P', 'GoBack']`; action contexts\n * decide which one fires. While this is on, back never closes the app by itself.\n * `false` leaves Android's default behavior alone. Default `'GoBack'` (the DOM\n * standard name for this key).\n */\n backButtonKey?: string | false;\n}\n\n/**\n * Public contract for the Tauri plugin. `save`/`load` make it storage-capable, so\n * it can be named as a `app.store` adapter — on native *and* on the web.\n */\nexport interface ITauriPlugin extends IPlugin<TauriPluginOptions> {\n /** Whether the app is actually running inside a Tauri webview. */\n readonly isNative: boolean;\n save(key: string, data: any): Promise<void>;\n load<T = any>(key: string): Promise<T | undefined>;\n /** Close the native window. A logged no-op on the web. */\n quit(): Promise<void>;\n}\n\n/** Prefix for the web (`localStorage`) fallback keys. */\nconst WEB_KEY_PREFIX = 'caper:';\n\n/**\n * Read at `initialize` time rather than module-eval time so `isDev` reflects the\n * build the plugin actually runs in.\n */\nfunction defaultOptions(): Required<TauriPluginOptions> {\n return {\n pauseOnHide: true,\n pauseOnBlur: false,\n nativeFullscreen: true,\n disableContextMenu: !isDev,\n storeFile: 'caper-save.json',\n backButtonKey: 'GoBack',\n };\n}\n\n/**\n * Native-shell integration for apps packaged with Tauri v2.\n *\n * Everything native is reached through `await import(...)` behind an `isTauri`\n * guard, so the same `plugins: [...]` list loads unchanged on the plain web —\n * where the plugin is inert apart from `save`/`load`, which fall back to\n * `localStorage`.\n *\n * What it does on native:\n * - pauses the app when the webview is hidden (a hidden Tauri window freezes\n * `requestAnimationFrame` and timers outright), and optionally on blur;\n * - installs a `FullscreenDriver` on `app.fullScreen`, because Tauri's webview\n * has no HTML Fullscreen API at all;\n * - backs `save`/`load` with `@tauri-apps/plugin-store`, a real file on disk,\n * rather than `localStorage` (which WebKit may evict);\n * - suppresses the webview's own right-click menu.\n *\n * @example\n * ```ts\n * // caper.config.ts\n * plugins: [['tauri', { options: { pauseOnBlur: true } }]],\n *\n * // anywhere\n * await app.store.save('tauri', 'progress', { level: 3 });\n * ```\n */\nexport class TauriPlugin extends Plugin<TauriPluginOptions> implements ITauriPlugin {\n public readonly id = 'tauri';\n /** `app.fullScreen` must exist before `postInitialize` installs the driver. */\n public readonly requires = ['fullscreen'];\n\n protected _options: Required<TauriPluginOptions>;\n\n /** The native window handle, resolved once on native. */\n private _window: TauriWindow | null = null;\n /** In-flight/settled store open, so the file is opened exactly once. */\n private _storePromise: Promise<TauriStore> | null = null;\n /** Whether *this* plugin is the one that paused the app. */\n private _didPause = false;\n /** The `notify` callback handed over by the fullscreen driver's `subscribe`. */\n private _notifyFullscreen: ((isFullscreen: boolean) => void) | null = null;\n /** So the off-native `quit()` warning is logged once, not once per call. */\n private _warnedQuit = false;\n\n /** Whether the app is actually running inside a Tauri webview. */\n public get isNative(): boolean {\n return isTauri;\n }\n\n public async initialize(options: Partial<TauriPluginOptions>, _app: IApplication): Promise<void> {\n this._options = { ...defaultOptions(), ...options };\n\n if (!isTauri) {\n return;\n }\n\n if (this._options.disableContextMenu) {\n this.listen(document, 'contextmenu', (event) => event.preventDefault());\n }\n\n if (this._options.pauseOnHide) {\n this.listen(document, 'visibilitychange', () => {\n if (document.visibilityState === 'hidden') {\n this._pause();\n } else {\n this._resume();\n }\n });\n }\n\n const win = await this._nativeWindow();\n\n if (this._options.pauseOnBlur) {\n this.addDisposer(\n await win.onFocusChanged(({ payload: focused }) => {\n if (focused) {\n this._resume();\n } else {\n this._pause();\n }\n }),\n );\n }\n\n await this._listenLifecycle();\n await this._listenBackButton();\n }\n\n public async postInitialize(_app: IApplication): Promise<void> {\n // Desktop only. Tauri's window.setFullscreen() rejects on Android/iOS, where the\n // webview's own HTML Fullscreen API works (and the Android shell already hides the\n // system bars), so on mobile core's DOM path is the one to keep.\n if (!isTauri || isMobile || !this._options.nativeFullscreen) {\n return;\n }\n\n const fullScreen = this.app.fullScreen;\n const win = await this._nativeWindow();\n\n // Tauri has no fullscreen-change event; a resize always accompanies the\n // transition, so re-query on resize and report only what changed.\n this.addDisposer(\n await win.onResized(async () => {\n if (!this._notifyFullscreen) {\n return;\n }\n try {\n this._notifyFullscreen(await win.isFullscreen());\n } catch (error) {\n Logger.error(`[${this.id}] could not read the native fullscreen state:`, error);\n }\n }),\n );\n\n const driver: FullscreenDriver = {\n supported: true,\n request: () => win.setFullscreen(true),\n exit: () => win.setFullscreen(false),\n subscribe: (notify) => {\n this._notifyFullscreen = notify;\n return () => {\n this._notifyFullscreen = null;\n };\n },\n };\n fullScreen.setFullscreenDriver(driver);\n this.addDisposer(() => fullScreen.setFullscreenDriver(null));\n }\n\n /**\n * Persist `data` under `key` — to the disk-backed store on native, to\n * `localStorage` on the web. Rejects (never swallows) on failure.\n */\n public async save(key: string, data: any): Promise<void> {\n if (!isTauri) {\n try {\n localStorage.setItem(`${WEB_KEY_PREFIX}${key}`, JSON.stringify(data));\n } catch (error) {\n throw new Error(`[${this.id}] failed to save \"${key}\" to localStorage: ${describe(error)}`);\n }\n return;\n }\n\n try {\n const store = await this._store();\n await store.set(key, data);\n await store.save();\n } catch (error) {\n throw new Error(`[${this.id}] failed to save \"${key}\" to ${this._options.storeFile}: ${describe(error)}`);\n }\n }\n\n /**\n * Read back what `save` wrote. A missing key resolves `undefined`; so does a\n * corrupt `localStorage` entry on the web.\n */\n public async load<T = any>(key: string): Promise<T | undefined> {\n if (!isTauri) {\n const raw = localStorage.getItem(`${WEB_KEY_PREFIX}${key}`);\n if (raw === null) {\n return undefined;\n }\n try {\n return JSON.parse(raw) as T;\n } catch {\n return undefined;\n }\n }\n\n try {\n const store = await this._store();\n return await store.get<T>(key);\n } catch (error) {\n throw new Error(`[${this.id}] failed to load \"${key}\" from ${this._options.storeFile}: ${describe(error)}`);\n }\n }\n\n /** Close the native window. A logged no-op on the web. */\n public async quit(): Promise<void> {\n if (!isTauri) {\n if (!this._warnedQuit) {\n this._warnedQuit = true;\n Logger.warn(`[${this.id}] quit() does nothing outside a Tauri window`);\n }\n return;\n }\n const win = await this._nativeWindow();\n await win.close();\n }\n\n /** Resolve (once) the native window handle. Only ever called when `isTauri`. */\n private async _nativeWindow(): Promise<TauriWindow> {\n if (!this._window) {\n const { getCurrentWindow } = await import('@tauri-apps/api/window');\n this._window = getCurrentWindow();\n }\n return this._window;\n }\n\n /** Open (once) the disk-backed store. Only ever called when `isTauri`. */\n private _store(): Promise<TauriStore> {\n if (!this._storePromise) {\n this._storePromise = (async () => {\n const { load } = await import('@tauri-apps/plugin-store');\n return load(this._options.storeFile, { autoSave: true });\n })();\n // a failed open must not be cached, or every later call sees the same error\n this._storePromise.catch(() => {\n this._storePromise = null;\n });\n }\n return this._storePromise;\n }\n\n /**\n * Mobile webviews are suspended rather than hidden, so `visibilitychange` is\n * not enough on iOS/Android. These events exist from `@tauri-apps/api` v2.\n */\n /**\n * Android's back button, delivered as an ordinary key press. Caper's keyboard\n * controls read keys off the document, so the game's controls config and action\n * contexts decide what back does; nothing is hard-coded here.\n */\n private async _listenBackButton(): Promise<void> {\n const key = this._options.backButtonKey;\n if (!isAndroid || !key) {\n return;\n }\n try {\n const { onBackButtonPress } = await import('@tauri-apps/api/app');\n const listener = await onBackButtonPress(() => {\n document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));\n document.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));\n });\n this.addDisposer(() => void listener.unregister());\n } catch (error) {\n Logger.error(`[${this.id}] could not listen for the Android back button:`, error);\n }\n }\n\n private async _listenLifecycle(): Promise<void> {\n const { listen, TauriEvent } = await import('@tauri-apps/api/event');\n const suspended = (TauriEvent as Record<string, string>).WINDOW_SUSPENDED;\n const resumed = (TauriEvent as Record<string, string>).WINDOW_RESUMED;\n if (!suspended || !resumed) {\n return;\n }\n const unlisten: UnlistenFn[] = await Promise.all([\n listen(suspended, () => this._pause()),\n listen(resumed, () => this._resume()),\n ]);\n this.addDisposer(...unlisten);\n }\n\n /** Pause the app, but only if nothing else already did — and remember that we did. */\n private _pause(): void {\n if (this.app.paused) {\n return;\n }\n this.app.pause();\n this._didPause = true;\n }\n\n /** Resume the app, but only if this plugin is the one that paused it. */\n private _resume(): void {\n if (!this._didPause) {\n return;\n }\n this._didPause = false;\n this.app.resume();\n }\n}\n\n/** Message text from whatever a rejected Tauri call threw. */\nfunction describe(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport { version };\nexport default TauriPlugin;\n","import { TauriPlugin } from './TauriPlugin';\n\nexport * from './TauriPlugin';\n\nexport default TauriPlugin;\n"],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { TauriPlugin } from './TauriPlugin';
2
+ export * from './TauriPlugin';
3
+ export default TauriPlugin;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,cAAc,eAAe,CAAC;AAE9B,eAAe,WAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const version = "0.7.2";
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@caperjs/plugin-tauri",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Tauri native shell integration",
4
+ "keywords": [
5
+ "tauri",
6
+ "native",
7
+ "desktop",
8
+ "@caperjs/core",
9
+ "caper"
10
+ ],
11
+ "version": "0.7.2",
12
+ "files": [
13
+ "lib",
14
+ "src",
15
+ "!src/**/*.test.ts"
16
+ ],
17
+ "main": "lib/caper-plugin-tauri.mjs",
18
+ "types": "lib/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "default": "./lib/caper-plugin-tauri.mjs",
22
+ "types": "./lib/index.d.ts"
23
+ }
24
+ },
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "pnpm run generate-version && vite build",
28
+ "test": "vitest run",
29
+ "generate-version": "node generateVersion.mjs"
30
+ },
31
+ "author": "",
32
+ "license": "MIT",
33
+ "devDependencies": {
34
+ "@caperjs/core": "^0.7.2",
35
+ "@tauri-apps/api": "^2",
36
+ "@tauri-apps/plugin-store": "^2",
37
+ "happy-dom": "^15",
38
+ "vite": "^8.0.7",
39
+ "vite-plugin-dts": "^4.5.3",
40
+ "vitest": "^3"
41
+ },
42
+ "peerDependencies": {
43
+ "@caperjs/core": "^0.7.2",
44
+ "@tauri-apps/api": "^2",
45
+ "@tauri-apps/plugin-store": "^2"
46
+ }
6
47
  }
@@ -0,0 +1,350 @@
1
+ import {
2
+ type FullscreenDriver,
3
+ type IApplication,
4
+ type IPlugin,
5
+ isAndroid,
6
+ isDev,
7
+ isMobile,
8
+ isTauri,
9
+ Logger,
10
+ Plugin,
11
+ } from '@caperjs/core';
12
+ import type { UnlistenFn } from '@tauri-apps/api/event';
13
+ import type { Window as TauriWindow } from '@tauri-apps/api/window';
14
+ import type { Store as TauriStore } from '@tauri-apps/plugin-store';
15
+ import { version } from './version';
16
+
17
+ /** Options for {@link TauriPlugin}, all optional. */
18
+ export interface TauriPluginOptions {
19
+ /** Pause the app while the webview is hidden/occluded. Default `true`. */
20
+ pauseOnHide?: boolean;
21
+ /** Pause the app while the native window is unfocused. Default `false`. */
22
+ pauseOnBlur?: boolean;
23
+ /** Drive `app.fullScreen` through the native window. Default `true`. */
24
+ nativeFullscreen?: boolean;
25
+ /** Swallow the webview's right-click menu. Default: `true` in production, `false` in dev. */
26
+ disableContextMenu?: boolean;
27
+ /** File the disk-backed key/value store lives in, under the app data dir. Default `'caper-save.json'`. */
28
+ storeFile?: string;
29
+ /**
30
+ * Android only: the keyboard key a back-button press (or back gesture) is delivered
31
+ * as. Bind it in your controls config like any key, e.g.
32
+ * `close: ['Escape', 'GoBack']`, `toggle_pause: ['P', 'GoBack']`; action contexts
33
+ * decide which one fires. While this is on, back never closes the app by itself.
34
+ * `false` leaves Android's default behavior alone. Default `'GoBack'` (the DOM
35
+ * standard name for this key).
36
+ */
37
+ backButtonKey?: string | false;
38
+ }
39
+
40
+ /**
41
+ * Public contract for the Tauri plugin. `save`/`load` make it storage-capable, so
42
+ * it can be named as a `app.store` adapter — on native *and* on the web.
43
+ */
44
+ export interface ITauriPlugin extends IPlugin<TauriPluginOptions> {
45
+ /** Whether the app is actually running inside a Tauri webview. */
46
+ readonly isNative: boolean;
47
+ save(key: string, data: any): Promise<void>;
48
+ load<T = any>(key: string): Promise<T | undefined>;
49
+ /** Close the native window. A logged no-op on the web. */
50
+ quit(): Promise<void>;
51
+ }
52
+
53
+ /** Prefix for the web (`localStorage`) fallback keys. */
54
+ const WEB_KEY_PREFIX = 'caper:';
55
+
56
+ /**
57
+ * Read at `initialize` time rather than module-eval time so `isDev` reflects the
58
+ * build the plugin actually runs in.
59
+ */
60
+ function defaultOptions(): Required<TauriPluginOptions> {
61
+ return {
62
+ pauseOnHide: true,
63
+ pauseOnBlur: false,
64
+ nativeFullscreen: true,
65
+ disableContextMenu: !isDev,
66
+ storeFile: 'caper-save.json',
67
+ backButtonKey: 'GoBack',
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Native-shell integration for apps packaged with Tauri v2.
73
+ *
74
+ * Everything native is reached through `await import(...)` behind an `isTauri`
75
+ * guard, so the same `plugins: [...]` list loads unchanged on the plain web —
76
+ * where the plugin is inert apart from `save`/`load`, which fall back to
77
+ * `localStorage`.
78
+ *
79
+ * What it does on native:
80
+ * - pauses the app when the webview is hidden (a hidden Tauri window freezes
81
+ * `requestAnimationFrame` and timers outright), and optionally on blur;
82
+ * - installs a `FullscreenDriver` on `app.fullScreen`, because Tauri's webview
83
+ * has no HTML Fullscreen API at all;
84
+ * - backs `save`/`load` with `@tauri-apps/plugin-store`, a real file on disk,
85
+ * rather than `localStorage` (which WebKit may evict);
86
+ * - suppresses the webview's own right-click menu.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * // caper.config.ts
91
+ * plugins: [['tauri', { options: { pauseOnBlur: true } }]],
92
+ *
93
+ * // anywhere
94
+ * await app.store.save('tauri', 'progress', { level: 3 });
95
+ * ```
96
+ */
97
+ export class TauriPlugin extends Plugin<TauriPluginOptions> implements ITauriPlugin {
98
+ public readonly id = 'tauri';
99
+ /** `app.fullScreen` must exist before `postInitialize` installs the driver. */
100
+ public readonly requires = ['fullscreen'];
101
+
102
+ protected _options: Required<TauriPluginOptions>;
103
+
104
+ /** The native window handle, resolved once on native. */
105
+ private _window: TauriWindow | null = null;
106
+ /** In-flight/settled store open, so the file is opened exactly once. */
107
+ private _storePromise: Promise<TauriStore> | null = null;
108
+ /** Whether *this* plugin is the one that paused the app. */
109
+ private _didPause = false;
110
+ /** The `notify` callback handed over by the fullscreen driver's `subscribe`. */
111
+ private _notifyFullscreen: ((isFullscreen: boolean) => void) | null = null;
112
+ /** So the off-native `quit()` warning is logged once, not once per call. */
113
+ private _warnedQuit = false;
114
+
115
+ /** Whether the app is actually running inside a Tauri webview. */
116
+ public get isNative(): boolean {
117
+ return isTauri;
118
+ }
119
+
120
+ public async initialize(options: Partial<TauriPluginOptions>, _app: IApplication): Promise<void> {
121
+ this._options = { ...defaultOptions(), ...options };
122
+
123
+ if (!isTauri) {
124
+ return;
125
+ }
126
+
127
+ if (this._options.disableContextMenu) {
128
+ this.listen(document, 'contextmenu', (event) => event.preventDefault());
129
+ }
130
+
131
+ if (this._options.pauseOnHide) {
132
+ this.listen(document, 'visibilitychange', () => {
133
+ if (document.visibilityState === 'hidden') {
134
+ this._pause();
135
+ } else {
136
+ this._resume();
137
+ }
138
+ });
139
+ }
140
+
141
+ const win = await this._nativeWindow();
142
+
143
+ if (this._options.pauseOnBlur) {
144
+ this.addDisposer(
145
+ await win.onFocusChanged(({ payload: focused }) => {
146
+ if (focused) {
147
+ this._resume();
148
+ } else {
149
+ this._pause();
150
+ }
151
+ }),
152
+ );
153
+ }
154
+
155
+ await this._listenLifecycle();
156
+ await this._listenBackButton();
157
+ }
158
+
159
+ public async postInitialize(_app: IApplication): Promise<void> {
160
+ // Desktop only. Tauri's window.setFullscreen() rejects on Android/iOS, where the
161
+ // webview's own HTML Fullscreen API works (and the Android shell already hides the
162
+ // system bars), so on mobile core's DOM path is the one to keep.
163
+ if (!isTauri || isMobile || !this._options.nativeFullscreen) {
164
+ return;
165
+ }
166
+
167
+ const fullScreen = this.app.fullScreen;
168
+ const win = await this._nativeWindow();
169
+
170
+ // Tauri has no fullscreen-change event; a resize always accompanies the
171
+ // transition, so re-query on resize and report only what changed.
172
+ this.addDisposer(
173
+ await win.onResized(async () => {
174
+ if (!this._notifyFullscreen) {
175
+ return;
176
+ }
177
+ try {
178
+ this._notifyFullscreen(await win.isFullscreen());
179
+ } catch (error) {
180
+ Logger.error(`[${this.id}] could not read the native fullscreen state:`, error);
181
+ }
182
+ }),
183
+ );
184
+
185
+ const driver: FullscreenDriver = {
186
+ supported: true,
187
+ request: () => win.setFullscreen(true),
188
+ exit: () => win.setFullscreen(false),
189
+ subscribe: (notify) => {
190
+ this._notifyFullscreen = notify;
191
+ return () => {
192
+ this._notifyFullscreen = null;
193
+ };
194
+ },
195
+ };
196
+ fullScreen.setFullscreenDriver(driver);
197
+ this.addDisposer(() => fullScreen.setFullscreenDriver(null));
198
+ }
199
+
200
+ /**
201
+ * Persist `data` under `key` — to the disk-backed store on native, to
202
+ * `localStorage` on the web. Rejects (never swallows) on failure.
203
+ */
204
+ public async save(key: string, data: any): Promise<void> {
205
+ if (!isTauri) {
206
+ try {
207
+ localStorage.setItem(`${WEB_KEY_PREFIX}${key}`, JSON.stringify(data));
208
+ } catch (error) {
209
+ throw new Error(`[${this.id}] failed to save "${key}" to localStorage: ${describe(error)}`);
210
+ }
211
+ return;
212
+ }
213
+
214
+ try {
215
+ const store = await this._store();
216
+ await store.set(key, data);
217
+ await store.save();
218
+ } catch (error) {
219
+ throw new Error(`[${this.id}] failed to save "${key}" to ${this._options.storeFile}: ${describe(error)}`);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Read back what `save` wrote. A missing key resolves `undefined`; so does a
225
+ * corrupt `localStorage` entry on the web.
226
+ */
227
+ public async load<T = any>(key: string): Promise<T | undefined> {
228
+ if (!isTauri) {
229
+ const raw = localStorage.getItem(`${WEB_KEY_PREFIX}${key}`);
230
+ if (raw === null) {
231
+ return undefined;
232
+ }
233
+ try {
234
+ return JSON.parse(raw) as T;
235
+ } catch {
236
+ return undefined;
237
+ }
238
+ }
239
+
240
+ try {
241
+ const store = await this._store();
242
+ return await store.get<T>(key);
243
+ } catch (error) {
244
+ throw new Error(`[${this.id}] failed to load "${key}" from ${this._options.storeFile}: ${describe(error)}`);
245
+ }
246
+ }
247
+
248
+ /** Close the native window. A logged no-op on the web. */
249
+ public async quit(): Promise<void> {
250
+ if (!isTauri) {
251
+ if (!this._warnedQuit) {
252
+ this._warnedQuit = true;
253
+ Logger.warn(`[${this.id}] quit() does nothing outside a Tauri window`);
254
+ }
255
+ return;
256
+ }
257
+ const win = await this._nativeWindow();
258
+ await win.close();
259
+ }
260
+
261
+ /** Resolve (once) the native window handle. Only ever called when `isTauri`. */
262
+ private async _nativeWindow(): Promise<TauriWindow> {
263
+ if (!this._window) {
264
+ const { getCurrentWindow } = await import('@tauri-apps/api/window');
265
+ this._window = getCurrentWindow();
266
+ }
267
+ return this._window;
268
+ }
269
+
270
+ /** Open (once) the disk-backed store. Only ever called when `isTauri`. */
271
+ private _store(): Promise<TauriStore> {
272
+ if (!this._storePromise) {
273
+ this._storePromise = (async () => {
274
+ const { load } = await import('@tauri-apps/plugin-store');
275
+ return load(this._options.storeFile, { autoSave: true });
276
+ })();
277
+ // a failed open must not be cached, or every later call sees the same error
278
+ this._storePromise.catch(() => {
279
+ this._storePromise = null;
280
+ });
281
+ }
282
+ return this._storePromise;
283
+ }
284
+
285
+ /**
286
+ * Mobile webviews are suspended rather than hidden, so `visibilitychange` is
287
+ * not enough on iOS/Android. These events exist from `@tauri-apps/api` v2.
288
+ */
289
+ /**
290
+ * Android's back button, delivered as an ordinary key press. Caper's keyboard
291
+ * controls read keys off the document, so the game's controls config and action
292
+ * contexts decide what back does; nothing is hard-coded here.
293
+ */
294
+ private async _listenBackButton(): Promise<void> {
295
+ const key = this._options.backButtonKey;
296
+ if (!isAndroid || !key) {
297
+ return;
298
+ }
299
+ try {
300
+ const { onBackButtonPress } = await import('@tauri-apps/api/app');
301
+ const listener = await onBackButtonPress(() => {
302
+ document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
303
+ document.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
304
+ });
305
+ this.addDisposer(() => void listener.unregister());
306
+ } catch (error) {
307
+ Logger.error(`[${this.id}] could not listen for the Android back button:`, error);
308
+ }
309
+ }
310
+
311
+ private async _listenLifecycle(): Promise<void> {
312
+ const { listen, TauriEvent } = await import('@tauri-apps/api/event');
313
+ const suspended = (TauriEvent as Record<string, string>).WINDOW_SUSPENDED;
314
+ const resumed = (TauriEvent as Record<string, string>).WINDOW_RESUMED;
315
+ if (!suspended || !resumed) {
316
+ return;
317
+ }
318
+ const unlisten: UnlistenFn[] = await Promise.all([
319
+ listen(suspended, () => this._pause()),
320
+ listen(resumed, () => this._resume()),
321
+ ]);
322
+ this.addDisposer(...unlisten);
323
+ }
324
+
325
+ /** Pause the app, but only if nothing else already did — and remember that we did. */
326
+ private _pause(): void {
327
+ if (this.app.paused) {
328
+ return;
329
+ }
330
+ this.app.pause();
331
+ this._didPause = true;
332
+ }
333
+
334
+ /** Resume the app, but only if this plugin is the one that paused it. */
335
+ private _resume(): void {
336
+ if (!this._didPause) {
337
+ return;
338
+ }
339
+ this._didPause = false;
340
+ this.app.resume();
341
+ }
342
+ }
343
+
344
+ /** Message text from whatever a rejected Tauri call threw. */
345
+ function describe(error: unknown): string {
346
+ return error instanceof Error ? error.message : String(error);
347
+ }
348
+
349
+ export { version };
350
+ export default TauriPlugin;
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { TauriPlugin } from './TauriPlugin';
2
+
3
+ export * from './TauriPlugin';
4
+
5
+ export default TauriPlugin;
package/src/version.ts ADDED
@@ -0,0 +1 @@
1
+ export const version = '0.7.2';