@rdlabo/ionic-angular-kit 0.0.14 → 0.0.16

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.
@@ -0,0 +1,163 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, inject, DOCUMENT, Injectable } from '@angular/core';
3
+ import { Capacitor } from '@capacitor/core';
4
+ import { StatusBar, Style } from '@capacitor/status-bar';
5
+ import { BehaviorSubject } from 'rxjs';
6
+ import { KitStorageService } from '@rdlabo/ionic-angular-kit';
7
+
8
+ /**
9
+ * Injection token carrying the {@link KitThemeConfig} for `KitThemeController`.
10
+ *
11
+ * @remarks
12
+ * Provide it through {@link provideKitTheme} rather than registering it directly.
13
+ */
14
+ const KIT_THEME_CONFIG = new InjectionToken('@rdlabo/ionic-angular-kit:theme');
15
+ /**
16
+ * Wire `KitThemeController` into the application.
17
+ *
18
+ * @param config - theme configuration: the storage key and the light/dark class lists
19
+ * @returns environment providers to add to the application's provider list
20
+ * @example
21
+ * ```ts
22
+ * bootstrapApplication(AppComponent, {
23
+ * providers: [
24
+ * provideKitTheme({
25
+ * storageKey: StorageKeyEnum.theme,
26
+ * darkClasses: ['ion-palette-dark', 'a2ui-dark'],
27
+ * lightClasses: ['a2ui-light'],
28
+ * }),
29
+ * ],
30
+ * });
31
+ * ```
32
+ */
33
+ const provideKitTheme = (config) => makeEnvironmentProviders([{ provide: KIT_THEME_CONFIG, useValue: config }]);
34
+
35
+ /**
36
+ * Light/dark theme controller: persists the user's choice, follows the OS setting until the user
37
+ * overrides it, toggles the configured palette classes, and syncs the native Android status bar.
38
+ *
39
+ * @remarks
40
+ * Consolidates the theme logic that had drifted across the fleet into one behavior. Notably it fixes
41
+ * a latent leak in one variant where the system-theme listener stayed registered after a manual
42
+ * toggle: {@link changeTheme} always detaches the listener via {@link removeEventListener} before
43
+ * applying the forced theme, so a later OS change can no longer silently flip an app the user pinned.
44
+ *
45
+ * - **Persistence** — the chosen mode is stored via {@link KitStorageService} under the configured key.
46
+ * - **Follow OS until overridden** — on boot with nothing stored, it tracks
47
+ * `prefers-color-scheme` (idempotent registration); once the user calls {@link changeTheme} it stops
48
+ * following and honors the explicit choice.
49
+ * - **Class toggling** — toggles {@link KitThemeConfig.darkClasses} on when dark and
50
+ * {@link KitThemeConfig.lightClasses} on when light, absorbing per-app CSS differences via config.
51
+ * - **Native status bar** — on Android native only, mirrors the Ionic behavior of setting the status
52
+ * bar style to match (iOS derives it from the web content, so it is intentionally left untouched).
53
+ *
54
+ * Subscribe to {@link themeSubject} to reflect the current mode in the UI (e.g. a settings toggle).
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * // On boot (app.component):
59
+ * inject(KitThemeController).setDefaultThemeMode();
60
+ *
61
+ * // From a settings toggle:
62
+ * const theme = inject(KitThemeController);
63
+ * theme.themeSubject.subscribe((mode) => this.isDark.set(mode === 'dark'));
64
+ * theme.changeTheme(true);
65
+ * ```
66
+ */
67
+ class KitThemeController {
68
+ constructor() {
69
+ this.#storage = inject(KitStorageService);
70
+ this.#document = inject(DOCUMENT);
71
+ this.#config = inject(KIT_THEME_CONFIG);
72
+ /**
73
+ * Emits the active theme, seeded with `'light'`.
74
+ *
75
+ * @remarks
76
+ * A `BehaviorSubject`, so a late subscriber immediately receives the current mode; it emits again
77
+ * on every {@link setDefaultThemeMode} / {@link changeTheme} and on OS theme changes while following.
78
+ */
79
+ this.themeSubject = new BehaviorSubject('light');
80
+ }
81
+ #storage;
82
+ #document;
83
+ #config;
84
+ #prefersDark;
85
+ #onSystemThemeChange;
86
+ /**
87
+ * Apply the persisted theme, or start following the OS setting when nothing is stored yet.
88
+ *
89
+ * @remarks
90
+ * Call once on boot (e.g. from `app.component`).
91
+ *
92
+ * @returns a Promise that resolves once the initial theme has been applied
93
+ */
94
+ async setDefaultThemeMode() {
95
+ const stored = await this.#storage.get(this.#config.storageKey);
96
+ if (stored) {
97
+ // 保存済みの選択を強制し、OS 追従は解除する。
98
+ this.#unwatchSystemTheme();
99
+ return this.#applyTheme(stored === 'dark');
100
+ }
101
+ // 未保存 → OS の設定に追従する。
102
+ this.#watchSystemTheme();
103
+ }
104
+ /**
105
+ * Force a theme, persist it, and stop following the OS setting.
106
+ *
107
+ * @param isDark - `true` for the dark theme, `false` for light
108
+ * @returns a Promise that resolves once the theme has been persisted and applied
109
+ */
110
+ async changeTheme(isDark) {
111
+ this.#unwatchSystemTheme();
112
+ await this.#storage.set(this.#config.storageKey, isDark ? 'dark' : 'light');
113
+ await this.#applyTheme(isDark);
114
+ }
115
+ #watchSystemTheme() {
116
+ if (this.#prefersDark) {
117
+ // 既に監視中なら二重登録しない(冪等)。
118
+ return;
119
+ }
120
+ this.#prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
121
+ this.#onSystemThemeChange = (e) => void this.#applyTheme(e.matches);
122
+ this.#prefersDark.addEventListener('change', this.#onSystemThemeChange, { passive: true });
123
+ void this.#applyTheme(this.#prefersDark.matches);
124
+ }
125
+ #unwatchSystemTheme() {
126
+ if (this.#prefersDark && this.#onSystemThemeChange) {
127
+ this.#prefersDark.removeEventListener('change', this.#onSystemThemeChange);
128
+ }
129
+ this.#prefersDark = undefined;
130
+ this.#onSystemThemeChange = undefined;
131
+ }
132
+ async #applyTheme(isDark) {
133
+ if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') {
134
+ await StatusBar.setStyle({ style: isDark ? Style.Dark : Style.Light });
135
+ }
136
+ const root = this.#document.documentElement;
137
+ for (const cls of this.#config.darkClasses) {
138
+ root.classList.toggle(cls, isDark);
139
+ }
140
+ for (const cls of this.#config.lightClasses) {
141
+ root.classList.toggle(cls, !isDark);
142
+ }
143
+ this.themeSubject.next(isDark ? 'dark' : 'light');
144
+ }
145
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitThemeController, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
146
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitThemeController, providedIn: 'root' }); }
147
+ }
148
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitThemeController, decorators: [{
149
+ type: Injectable,
150
+ args: [{
151
+ providedIn: 'root',
152
+ }]
153
+ }] });
154
+
155
+ // Theme: light/dark controller with OS-follow and native status-bar sync.
156
+ // Its own entry point so only apps that theme pull in `@capacitor/status-bar`.
157
+
158
+ /**
159
+ * Generated bundle index. Do not edit.
160
+ */
161
+
162
+ export { KIT_THEME_CONFIG, KitThemeController, provideKitTheme };
163
+ //# sourceMappingURL=rdlabo-ionic-angular-kit-theme.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rdlabo-ionic-angular-kit-theme.mjs","sources":["../../../projects/kit/theme/src/theme-config.ts","../../../projects/kit/theme/src/kit-theme.controller.ts","../../../projects/kit/theme/src/public-api.ts","../../../projects/kit/theme/src/rdlabo-ionic-angular-kit-theme.ts"],"sourcesContent":["import type { EnvironmentProviders } from '@angular/core';\nimport { InjectionToken, makeEnvironmentProviders } from '@angular/core';\n\n/**\n * Theme configuration injected via `provideKitTheme()` and consumed by `KitThemeController`.\n *\n * @remarks\n * All fields are required; the consuming application supplies a complete configuration so every app\n * in the fleet has the same shape. The class lists absorb the per-app CSS drift: the kit toggles\n * `darkClasses` on when dark and `lightClasses` on when light, so an app that only uses Ionic's\n * palette passes `darkClasses: ['ion-palette-dark'], lightClasses: []`, while an app with an extra\n * design-system palette adds its own classes (e.g. `darkClasses: ['ion-palette-dark', 'a2ui-dark']`,\n * `lightClasses: ['a2ui-light']`).\n */\nexport interface KitThemeConfig {\n /** Key under which the chosen theme (`'light'` | `'dark'`) is persisted via `KitStorageService`. */\n readonly storageKey: string;\n /** Classes toggled **on** the document element when the dark theme is active. */\n readonly darkClasses: readonly string[];\n /** Classes toggled **on** the document element when the light theme is active. */\n readonly lightClasses: readonly string[];\n}\n\n/**\n * Injection token carrying the {@link KitThemeConfig} for `KitThemeController`.\n *\n * @remarks\n * Provide it through {@link provideKitTheme} rather than registering it directly.\n */\nexport const KIT_THEME_CONFIG = new InjectionToken<KitThemeConfig>('@rdlabo/ionic-angular-kit:theme');\n\n/**\n * Wire `KitThemeController` into the application.\n *\n * @param config - theme configuration: the storage key and the light/dark class lists\n * @returns environment providers to add to the application's provider list\n * @example\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideKitTheme({\n * storageKey: StorageKeyEnum.theme,\n * darkClasses: ['ion-palette-dark', 'a2ui-dark'],\n * lightClasses: ['a2ui-light'],\n * }),\n * ],\n * });\n * ```\n */\nexport const provideKitTheme = (config: KitThemeConfig): EnvironmentProviders =>\n makeEnvironmentProviders([{ provide: KIT_THEME_CONFIG, useValue: config }]);\n","import { DOCUMENT, inject, Injectable } from '@angular/core';\nimport { Capacitor } from '@capacitor/core';\nimport { StatusBar, Style } from '@capacitor/status-bar';\nimport { BehaviorSubject } from 'rxjs';\n\nimport { KitStorageService } from '@rdlabo/ionic-angular-kit';\n\nimport { KIT_THEME_CONFIG } from './theme-config';\n\n/** The active theme mode. */\nexport type KitThemeMode = 'light' | 'dark';\n\n/**\n * Light/dark theme controller: persists the user's choice, follows the OS setting until the user\n * overrides it, toggles the configured palette classes, and syncs the native Android status bar.\n *\n * @remarks\n * Consolidates the theme logic that had drifted across the fleet into one behavior. Notably it fixes\n * a latent leak in one variant where the system-theme listener stayed registered after a manual\n * toggle: {@link changeTheme} always detaches the listener via {@link removeEventListener} before\n * applying the forced theme, so a later OS change can no longer silently flip an app the user pinned.\n *\n * - **Persistence** — the chosen mode is stored via {@link KitStorageService} under the configured key.\n * - **Follow OS until overridden** — on boot with nothing stored, it tracks\n * `prefers-color-scheme` (idempotent registration); once the user calls {@link changeTheme} it stops\n * following and honors the explicit choice.\n * - **Class toggling** — toggles {@link KitThemeConfig.darkClasses} on when dark and\n * {@link KitThemeConfig.lightClasses} on when light, absorbing per-app CSS differences via config.\n * - **Native status bar** — on Android native only, mirrors the Ionic behavior of setting the status\n * bar style to match (iOS derives it from the web content, so it is intentionally left untouched).\n *\n * Subscribe to {@link themeSubject} to reflect the current mode in the UI (e.g. a settings toggle).\n *\n * @example\n * ```ts\n * // On boot (app.component):\n * inject(KitThemeController).setDefaultThemeMode();\n *\n * // From a settings toggle:\n * const theme = inject(KitThemeController);\n * theme.themeSubject.subscribe((mode) => this.isDark.set(mode === 'dark'));\n * theme.changeTheme(true);\n * ```\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class KitThemeController {\n readonly #storage = inject(KitStorageService);\n readonly #document = inject(DOCUMENT);\n readonly #config = inject(KIT_THEME_CONFIG);\n\n /**\n * Emits the active theme, seeded with `'light'`.\n *\n * @remarks\n * A `BehaviorSubject`, so a late subscriber immediately receives the current mode; it emits again\n * on every {@link setDefaultThemeMode} / {@link changeTheme} and on OS theme changes while following.\n */\n readonly themeSubject = new BehaviorSubject<KitThemeMode>('light');\n\n #prefersDark?: MediaQueryList;\n #onSystemThemeChange?: (e: MediaQueryListEvent) => void;\n\n /**\n * Apply the persisted theme, or start following the OS setting when nothing is stored yet.\n *\n * @remarks\n * Call once on boot (e.g. from `app.component`).\n *\n * @returns a Promise that resolves once the initial theme has been applied\n */\n async setDefaultThemeMode(): Promise<void> {\n const stored = await this.#storage.get<KitThemeMode>(this.#config.storageKey);\n if (stored) {\n // 保存済みの選択を強制し、OS 追従は解除する。\n this.#unwatchSystemTheme();\n return this.#applyTheme(stored === 'dark');\n }\n\n // 未保存 → OS の設定に追従する。\n this.#watchSystemTheme();\n }\n\n /**\n * Force a theme, persist it, and stop following the OS setting.\n *\n * @param isDark - `true` for the dark theme, `false` for light\n * @returns a Promise that resolves once the theme has been persisted and applied\n */\n async changeTheme(isDark: boolean): Promise<void> {\n this.#unwatchSystemTheme();\n await this.#storage.set(this.#config.storageKey, isDark ? 'dark' : 'light');\n await this.#applyTheme(isDark);\n }\n\n #watchSystemTheme(): void {\n if (this.#prefersDark) {\n // 既に監視中なら二重登録しない(冪等)。\n return;\n }\n this.#prefersDark = window.matchMedia('(prefers-color-scheme: dark)');\n this.#onSystemThemeChange = (e) => void this.#applyTheme(e.matches);\n this.#prefersDark.addEventListener('change', this.#onSystemThemeChange, { passive: true });\n void this.#applyTheme(this.#prefersDark.matches);\n }\n\n #unwatchSystemTheme(): void {\n if (this.#prefersDark && this.#onSystemThemeChange) {\n this.#prefersDark.removeEventListener('change', this.#onSystemThemeChange);\n }\n this.#prefersDark = undefined;\n this.#onSystemThemeChange = undefined;\n }\n\n async #applyTheme(isDark: boolean): Promise<void> {\n if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') {\n await StatusBar.setStyle({ style: isDark ? Style.Dark : Style.Light });\n }\n const root = this.#document.documentElement;\n for (const cls of this.#config.darkClasses) {\n root.classList.toggle(cls, isDark);\n }\n for (const cls of this.#config.lightClasses) {\n root.classList.toggle(cls, !isDark);\n }\n this.themeSubject.next(isDark ? 'dark' : 'light');\n }\n}\n","// Theme: light/dark controller with OS-follow and native status-bar sync.\n// Its own entry point so only apps that theme pull in `@capacitor/status-bar`.\nexport * from './theme-config';\nexport * from './kit-theme.controller';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -1,20 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Injectable, InjectionToken, makeEnvironmentProviders, ElementRef, Directive, DOCUMENT } from '@angular/core';
2
+ import { inject, Injectable, InjectionToken, makeEnvironmentProviders, ElementRef, Directive } from '@angular/core';
3
3
  import { Storage } from '@ionic/storage-angular';
4
4
  import { ModalController, PopoverController, ToastController, AlertController, LoadingController, NavController } from '@ionic/angular/standalone';
5
5
  import { Capacitor } from '@capacitor/core';
6
6
  import { Keyboard } from '@capacitor/keyboard';
7
7
  import { ImpactStyle, Haptics } from '@capacitor/haptics';
8
- import { StatusBar, Style } from '@capacitor/status-bar';
9
- import { BehaviorSubject, from, throwError, retry, timer, Observable, startWith } from 'rxjs';
10
- import { Preferences } from '@capacitor/preferences';
11
- import { InAppReview } from '@capacitor-community/in-app-review';
12
- import { BRLMPrinterHalftone, BRLMPrinterCustomPaperUnit, BRLMPrinterCustomPaperType, BRLMPrinterPrintQuality, BRLMPrinterHorizontalAlignment, BRLMPrinterVerticalAlignment, BRLMPrinterImageRotation, BRLMPrinterScaleMode } from '@rdlabo/capacitor-brotherprint';
13
- import domtoimage from 'dom-to-image-more';
14
8
  import { Router } from '@angular/router';
15
9
  import { map, mergeMap, catchError, timeout, tap } from 'rxjs/operators';
16
10
  import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
17
11
  import { Network } from '@capacitor/network';
12
+ import { from, throwError, retry, timer, Observable, startWith } from 'rxjs';
18
13
 
19
14
  /**
20
15
  * Thin, typed wrapper around `@ionic/storage-angular`.
@@ -835,319 +830,6 @@ const kitKeyboardInit = async (elementRef, type) => {
835
830
  ];
836
831
  };
837
832
 
838
- /**
839
- * Injection token carrying the {@link KitThemeConfig} for `KitThemeController`.
840
- *
841
- * @remarks
842
- * Provide it through {@link provideKitTheme} rather than registering it directly.
843
- */
844
- const KIT_THEME_CONFIG = new InjectionToken('@rdlabo/ionic-angular-kit:theme');
845
- /**
846
- * Wire `KitThemeController` into the application.
847
- *
848
- * @param config - theme configuration: the storage key and the light/dark class lists
849
- * @returns environment providers to add to the application's provider list
850
- * @example
851
- * ```ts
852
- * bootstrapApplication(AppComponent, {
853
- * providers: [
854
- * provideKitTheme({
855
- * storageKey: StorageKeyEnum.theme,
856
- * darkClasses: ['ion-palette-dark', 'a2ui-dark'],
857
- * lightClasses: ['a2ui-light'],
858
- * }),
859
- * ],
860
- * });
861
- * ```
862
- */
863
- const provideKitTheme = (config) => makeEnvironmentProviders([{ provide: KIT_THEME_CONFIG, useValue: config }]);
864
-
865
- /**
866
- * Light/dark theme controller: persists the user's choice, follows the OS setting until the user
867
- * overrides it, toggles the configured palette classes, and syncs the native Android status bar.
868
- *
869
- * @remarks
870
- * Consolidates the theme logic that had drifted across the fleet into one behavior. Notably it fixes
871
- * a latent leak in one variant where the system-theme listener stayed registered after a manual
872
- * toggle: {@link changeTheme} always detaches the listener via {@link removeEventListener} before
873
- * applying the forced theme, so a later OS change can no longer silently flip an app the user pinned.
874
- *
875
- * - **Persistence** — the chosen mode is stored via {@link KitStorageService} under the configured key.
876
- * - **Follow OS until overridden** — on boot with nothing stored, it tracks
877
- * `prefers-color-scheme` (idempotent registration); once the user calls {@link changeTheme} it stops
878
- * following and honors the explicit choice.
879
- * - **Class toggling** — toggles {@link KitThemeConfig.darkClasses} on when dark and
880
- * {@link KitThemeConfig.lightClasses} on when light, absorbing per-app CSS differences via config.
881
- * - **Native status bar** — on Android native only, mirrors the Ionic behavior of setting the status
882
- * bar style to match (iOS derives it from the web content, so it is intentionally left untouched).
883
- *
884
- * Subscribe to {@link themeSubject} to reflect the current mode in the UI (e.g. a settings toggle).
885
- *
886
- * @example
887
- * ```ts
888
- * // On boot (app.component):
889
- * inject(KitThemeController).setDefaultThemeMode();
890
- *
891
- * // From a settings toggle:
892
- * const theme = inject(KitThemeController);
893
- * theme.themeSubject.subscribe((mode) => this.isDark.set(mode === 'dark'));
894
- * theme.changeTheme(true);
895
- * ```
896
- */
897
- class KitThemeController {
898
- constructor() {
899
- this.#storage = inject(KitStorageService);
900
- this.#document = inject(DOCUMENT);
901
- this.#config = inject(KIT_THEME_CONFIG);
902
- /**
903
- * Emits the active theme, seeded with `'light'`.
904
- *
905
- * @remarks
906
- * A `BehaviorSubject`, so a late subscriber immediately receives the current mode; it emits again
907
- * on every {@link setDefaultThemeMode} / {@link changeTheme} and on OS theme changes while following.
908
- */
909
- this.themeSubject = new BehaviorSubject('light');
910
- }
911
- #storage;
912
- #document;
913
- #config;
914
- #prefersDark;
915
- #onSystemThemeChange;
916
- /**
917
- * Apply the persisted theme, or start following the OS setting when nothing is stored yet.
918
- *
919
- * @remarks
920
- * Call once on boot (e.g. from `app.component`).
921
- *
922
- * @returns a Promise that resolves once the initial theme has been applied
923
- */
924
- async setDefaultThemeMode() {
925
- const stored = await this.#storage.get(this.#config.storageKey);
926
- if (stored) {
927
- // 保存済みの選択を強制し、OS 追従は解除する。
928
- this.#unwatchSystemTheme();
929
- return this.#applyTheme(stored === 'dark');
930
- }
931
- // 未保存 → OS の設定に追従する。
932
- this.#watchSystemTheme();
933
- }
934
- /**
935
- * Force a theme, persist it, and stop following the OS setting.
936
- *
937
- * @param isDark - `true` for the dark theme, `false` for light
938
- * @returns a Promise that resolves once the theme has been persisted and applied
939
- */
940
- async changeTheme(isDark) {
941
- this.#unwatchSystemTheme();
942
- await this.#storage.set(this.#config.storageKey, isDark ? 'dark' : 'light');
943
- await this.#applyTheme(isDark);
944
- }
945
- #watchSystemTheme() {
946
- if (this.#prefersDark) {
947
- // 既に監視中なら二重登録しない(冪等)。
948
- return;
949
- }
950
- this.#prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
951
- this.#onSystemThemeChange = (e) => void this.#applyTheme(e.matches);
952
- this.#prefersDark.addEventListener('change', this.#onSystemThemeChange, { passive: true });
953
- void this.#applyTheme(this.#prefersDark.matches);
954
- }
955
- #unwatchSystemTheme() {
956
- if (this.#prefersDark && this.#onSystemThemeChange) {
957
- this.#prefersDark.removeEventListener('change', this.#onSystemThemeChange);
958
- }
959
- this.#prefersDark = undefined;
960
- this.#onSystemThemeChange = undefined;
961
- }
962
- async #applyTheme(isDark) {
963
- if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') {
964
- await StatusBar.setStyle({ style: isDark ? Style.Dark : Style.Light });
965
- }
966
- const root = this.#document.documentElement;
967
- for (const cls of this.#config.darkClasses) {
968
- root.classList.toggle(cls, isDark);
969
- }
970
- for (const cls of this.#config.lightClasses) {
971
- root.classList.toggle(cls, !isDark);
972
- }
973
- this.themeSubject.next(isDark ? 'dark' : 'light');
974
- }
975
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitThemeController, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
976
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitThemeController, providedIn: 'root' }); }
977
- }
978
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: KitThemeController, decorators: [{
979
- type: Injectable,
980
- args: [{
981
- providedIn: 'root',
982
- }]
983
- }] });
984
-
985
- /**
986
- * Request the native in-app review dialog, throttled so the user is prompted at most once per window.
987
- *
988
- * @remarks
989
- * A plain function — no DI needed (`@capacitor/preferences`, `@capacitor-community/in-app-review` and
990
- * `Capacitor` are all static), so the caller invokes it directly and passes its own config rather
991
- * than injecting a controller. A no-op on non-native platforms. When enough time has elapsed since
992
- * the last prompt (per {@link KitRequestReviewOptions.throttleMonths}, tracked under
993
- * {@link KitRequestReviewOptions.storageKey}), it briefly waits for the app to settle, calls
994
- * `InAppReview.requestReview()`, and records the new timestamp. The wait/throttle/record sequence
995
- * was previously copy-pasted verbatim across the fleet; centralizing it means a single place to tune
996
- * the prompt cadence.
997
- *
998
- * @param options - the storage key and throttle window; see {@link KitRequestReviewOptions}
999
- * @returns a Promise that resolves once the request has been made (or immediately if throttled / on web)
1000
- * @example
1001
- * ```ts
1002
- * await kitRequestReview({ storageKey: StorageEnum.lastRequestRate, throttleMonths: 3 });
1003
- * ```
1004
- */
1005
- const kitRequestReview = async (options) => {
1006
- if (!Capacitor.isNativePlatform()) {
1007
- return;
1008
- }
1009
- await new Promise((resolve) => setTimeout(() => resolve(), 1000));
1010
- const threshold = new Date();
1011
- threshold.setMonth(threshold.getMonth() - options.throttleMonths);
1012
- const { value } = await Preferences.get({ key: options.storageKey });
1013
- if (!value || new Date(Number(value)).getTime() < threshold.getTime()) {
1014
- await InAppReview.requestReview();
1015
- await Preferences.set({ key: options.storageKey, value: new Date().getTime().toString() });
1016
- }
1017
- };
1018
-
1019
- /**
1020
- * Rotate a base64 image 90°, returning a new base64 data URL of the same MIME type.
1021
- *
1022
- * @remarks
1023
- * Pure DOM/canvas work — no DI. Used before sending a label to the printer when the artwork must be
1024
- * turned to match the tape orientation. Extracted verbatim from the fleet's printer services so the
1025
- * canvas handling lives in one place.
1026
- *
1027
- * @param imageData - a base64 data URL (e.g. `data:image/png;base64,...`)
1028
- * @returns a Promise resolving to the rotated image as a base64 data URL
1029
- */
1030
- const kitRotationImage = async (imageData) => {
1031
- const imgType = imageData.substring(5, imageData.indexOf(';'));
1032
- const image = new Image();
1033
- const loaded = new Promise((resolve) => {
1034
- image.onload = () => resolve();
1035
- });
1036
- image.src = imageData;
1037
- await loaded;
1038
- const canvas = document.createElement('canvas');
1039
- const ctx = canvas.getContext('2d');
1040
- canvas.width = image.height;
1041
- canvas.height = image.width;
1042
- ctx.rotate((90 * Math.PI) / 180);
1043
- ctx.translate(0, -image.height);
1044
- ctx.drawImage(image, 0, 0, image.width, image.height);
1045
- return canvas.toDataURL(imgType);
1046
- };
1047
- /**
1048
- * Render a DOM element to a base64 PNG for label printing, with the fleet's device-specific fixes.
1049
- *
1050
- * @remarks
1051
- * Pure function — no DI (reads the platform from `Capacitor`, uses the global `document`), so the
1052
- * caller presents its own loading UI around it. Centralizes the hard-won device quirks: on iOS it
1053
- * pads width/height by 2px (otherwise the bottom is clipped), on Android it does not (the padding
1054
- * introduces a black line). Retries the `dom-to-image-more` render up to 10 times because the first
1055
- * pass can occasionally return empty. This is exactly the kind of plumbing where a future fix should
1056
- * land in every app at once.
1057
- *
1058
- * @param element - the element to rasterize (e.g. the label preview host)
1059
- * @param options - rendering options; see {@link KitDomToPngOptions}
1060
- * @returns a Promise resolving to the PNG as a base64 data URL (empty string if every attempt failed)
1061
- * @example
1062
- * ```ts
1063
- * const loading = await this.#loadingCtrl.create({ message: this.text.generating });
1064
- * await loading.present();
1065
- * const png = await kitDomToPng(this.preview().nativeElement, { rotate: true });
1066
- * await loading.dismiss();
1067
- * ```
1068
- */
1069
- const kitDomToPng = async (element, options) => {
1070
- await new Promise((resolve) => requestAnimationFrame(resolve));
1071
- const { clientHeight, clientWidth } = element;
1072
- // デバイス毎の問題解決のため、px 調整。
1073
- // iOS: ないと下が途切れる。Android: あると黒線が入る。
1074
- const addClient = Capacitor.getPlatform() === 'ios' ? 2 : 0;
1075
- let dataUrl = '';
1076
- for (let i = 0; i < 10; i++) {
1077
- const url = await domtoimage.toPng(element, {
1078
- width: clientWidth + addClient,
1079
- height: clientHeight + addClient,
1080
- scale: options?.scale ?? 3,
1081
- copyDefaultStyles: false,
1082
- });
1083
- if (url) {
1084
- dataUrl = url;
1085
- break;
1086
- }
1087
- }
1088
- return options?.rotate ? kitRotationImage(dataUrl) : dataUrl;
1089
- };
1090
- /**
1091
- * Assemble the Brother `BRLMPrintOptions` for a die-cut label print, minus the transport fields.
1092
- *
1093
- * @remarks
1094
- * Pure function — no DI. Centralizes the fleet's canonical print settings (fit-page scale, centered,
1095
- * best quality, threshold halftone, 2mm/1mm margins, `gapLength` 2.0) and the tape sizing derived
1096
- * from the label's `W<width>H<height>` code. The caller merges the printer's `port` / `channelInfo`
1097
- * onto the result before calling `BrotherPrint.printImage()`, so channel selection and loading UI stay
1098
- * in the app.
1099
- *
1100
- * @param params - model, artwork, label, copies, and halftone threshold; see {@link KitBrotherPrintSettingsParams}
1101
- * @returns the `BRLMPrintOptions` ready to be spread with `{ port, channelInfo }`
1102
- * @example
1103
- * ```ts
1104
- * const settings = kitBuildBrotherPrintSettings({
1105
- * modelName, printBase64, label, numberOfCopies: printOptions.printNum, halftoneThreshold: printOptions.halftoneThreshold,
1106
- * });
1107
- * await BrotherPrint.printImage({ ...settings, port: channel.port, channelInfo: channel.channelInfo });
1108
- * ```
1109
- */
1110
- const kitBuildBrotherPrintSettings = (params) => {
1111
- const startPoint = params.printBase64.indexOf(',');
1112
- const tapeSize = params.label.match(/W(\d+)H(\d+)/);
1113
- const tapeWidth = tapeSize && tapeSize.length >= 2 ? parseInt(tapeSize[1], 10) : 0;
1114
- const tapeLength = tapeSize && tapeSize.length >= 3 ? parseInt(tapeSize[2], 10) : 0;
1115
- // `BRLMPrintOptions` is a `QL | TD` union; a die-cut label legitimately carries fields from both
1116
- // groups, so the object is composed via spreads to bypass the union's excess-property checks — the
1117
- // same technique the source printer services used.
1118
- return {
1119
- ...{
1120
- modelName: params.modelName,
1121
- encodedImage: params.printBase64.slice(startPoint + 1),
1122
- numberOfCopies: params.numberOfCopies,
1123
- autoCut: true,
1124
- scaleMode: BRLMPrinterScaleMode.FitPageAspect,
1125
- imageRotation: BRLMPrinterImageRotation.Rotate0,
1126
- verticalAlignment: BRLMPrinterVerticalAlignment.Center,
1127
- horizontalAlignment: BRLMPrinterHorizontalAlignment.Center,
1128
- printQuality: BRLMPrinterPrintQuality.Best,
1129
- },
1130
- ...{
1131
- labelName: params.label,
1132
- },
1133
- ...{
1134
- paperType: BRLMPrinterCustomPaperType.dieCutPaper,
1135
- paperUnit: BRLMPrinterCustomPaperUnit.mm,
1136
- halftone: BRLMPrinterHalftone.Threshold,
1137
- halftoneThreshold: params.halftoneThreshold,
1138
- tapeWidth: Number(tapeWidth.toFixed(1)),
1139
- tapeLength: Number(tapeLength.toFixed(1)),
1140
- gapLength: 2.0,
1141
- marginTop: 1.0,
1142
- marginRight: 2.0,
1143
- marginBottom: 1.0,
1144
- marginLeft: 2.0,
1145
- paperMarkPosition: 0,
1146
- paperMarkLength: 0,
1147
- },
1148
- };
1149
- };
1150
-
1151
833
  /**
1152
834
  * Injection token that carries the {@link KitAuthConfig} to the authentication guards.
1153
835
  */
@@ -1704,5 +1386,5 @@ const kitCreateDidEnter = (el) => {
1704
1386
  * Generated bundle index. Do not edit.
1705
1387
  */
1706
1388
 
1707
- export { KIT_AUTH_CONFIG, KIT_HTTP_CONFIG, KIT_OVERLAY_CONFIG, KIT_THEME_CONFIG, KitAutofillDirective, KitLoadingController, KitOverlayController, KitReloadAlertController, KitStorageService, KitThemeController, arrayConcatById, disableHandler, kitAuthInterceptor, kitBuildBrotherPrintSettings, kitChangeEventDisabled, kitCreateDidEnter, kitDomToPng, kitImpact, kitKeyboardInit, kitPresentAuthFailedAlert, kitPresentLanguageActionSheet, kitRequestReview, kitRequireAuthorizedGuard, kitRequireConfirmingGuard, kitRequiredUnauthorizedGuard, kitRotationImage, objectEqual, provideKitAuth, provideKitHttp, provideKitOverlay, provideKitTheme };
1389
+ export { KIT_AUTH_CONFIG, KIT_HTTP_CONFIG, KIT_OVERLAY_CONFIG, KitAutofillDirective, KitLoadingController, KitOverlayController, KitReloadAlertController, KitStorageService, arrayConcatById, disableHandler, kitAuthInterceptor, kitChangeEventDisabled, kitCreateDidEnter, kitImpact, kitKeyboardInit, kitPresentAuthFailedAlert, kitPresentLanguageActionSheet, kitRequireAuthorizedGuard, kitRequireConfirmingGuard, kitRequiredUnauthorizedGuard, objectEqual, provideKitAuth, provideKitHttp, provideKitOverlay };
1708
1390
  //# sourceMappingURL=rdlabo-ionic-angular-kit.mjs.map