@rdlabo/ionic-angular-photo-editor 20.0.3 → 21.0.1

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.
@@ -1,533 +0,0 @@
1
- import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
2
- import * as i0 from '@angular/core';
3
- import { inject, signal, input, effect, viewChild, ElementRef, computed, ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, Injectable } from '@angular/core';
4
- import { CommonModule } from '@angular/common';
5
- import { FormsModule } from '@angular/forms';
6
- import * as i1 from '@ionic/angular/standalone';
7
- import { IonHeader, IonToolbar, IonButtons, IonButton, IonIcon, IonContent, IonFooter, IonText, IonRange, ModalController, IonicSlides, ActionSheetController, Platform } from '@ionic/angular/standalone';
8
- import ImageEditor from 'tui-image-editor';
9
- import { toObservable } from '@angular/core/rxjs-interop';
10
- import { addIcons } from 'ionicons';
11
- import { removeOutline, closeOutline, checkmarkOutline, refreshOutline, squareOutline, tabletLandscapeOutline, expandOutline, sunnyOutline, colorFilterOutline, cropOutline, send } from 'ionicons/icons';
12
- import { Navigation, Zoom } from 'swiper/modules';
13
- import { Subscription, fromEvent, zipWith, withLatestFrom, throttleTime } from 'rxjs';
14
- import { register } from 'swiper/element/bundle';
15
- import { CameraSource, CameraResultType, Camera } from '@capacitor/camera';
16
-
17
- const filterPreset = (dictionary) => [
18
- {
19
- name: dictionary.original,
20
- type: 'Default',
21
- option: null,
22
- },
23
- // {
24
- // name: dictionary.invert,
25
- // type: 'Invert',
26
- // option: null,
27
- // },
28
- {
29
- name: dictionary.sepia,
30
- type: 'Sepia',
31
- option: null,
32
- },
33
- {
34
- name: dictionary.vintage,
35
- type: 'vintage',
36
- option: null,
37
- },
38
- {
39
- name: 'ぼかし',
40
- type: 'Blur',
41
- option: { blur: 0.1 },
42
- },
43
- {
44
- name: dictionary.grayscale,
45
- type: 'Grayscale',
46
- option: null,
47
- },
48
- // {
49
- // name: dictionary.sharpen,
50
- // type: 'Sharpen',
51
- // option: null,
52
- // },
53
- // {
54
- // name: dictionary.emboss,
55
- // type: 'Emboss',
56
- // option: null,
57
- // },
58
- ];
59
-
60
- const ionComponents = [IonHeader, IonToolbar, IonButtons, IonButton, IonIcon, IonContent, IonFooter, IonText, IonRange];
61
-
62
- const dictionaryForEditor = () => ({
63
- // UI labels
64
- save: '保存',
65
- crop: '切り抜き・回転',
66
- filter: 'フィルター',
67
- brightness: '明るさ',
68
- // Filter labels
69
- original: 'オリジナル',
70
- invert: '反転',
71
- sepia: 'セピア',
72
- vintage: 'ヴィンテージ',
73
- blur: 'ぼかし',
74
- grayscale: 'グレースケール',
75
- sharpen: '輪郭',
76
- emboss: 'エンボス',
77
- });
78
- const dictionaryForViewer = () => ({
79
- // UI labels
80
- delete: '削除',
81
- });
82
- const dictionaryForService = () => ({
83
- // UI labels
84
- camera: 'カメラ撮影',
85
- album: 'アルバムから選択',
86
- cancel: 'キャンセル',
87
- });
88
-
89
- const initializeViewerIcons = () => {
90
- addIcons({
91
- closeOutline,
92
- removeOutline,
93
- });
94
- };
95
- const initializeEditorIcons = () => {
96
- addIcons({
97
- closeOutline,
98
- send,
99
- cropOutline,
100
- colorFilterOutline,
101
- sunnyOutline,
102
- expandOutline,
103
- tabletLandscapeOutline,
104
- squareOutline,
105
- refreshOutline,
106
- checkmarkOutline,
107
- });
108
- };
109
- const waitToFindDom = (nativeElement, selector) => {
110
- return new Promise((resolve) => {
111
- const interval = setInterval(() => {
112
- const find = nativeElement.querySelector(selector);
113
- if (find) {
114
- clearInterval(interval);
115
- resolve();
116
- }
117
- });
118
- });
119
- };
120
-
121
- class PhotoEditorPage {
122
- constructor() {
123
- this.modalCtrl = inject(ModalController);
124
- this.dictionary = signal(dictionaryForEditor());
125
- this.requireSquare = input(false, {
126
- transform: coerceBooleanProperty,
127
- });
128
- this.labels = input(undefined);
129
- this.setLabels = effect(() => {
130
- if (this.labels()) {
131
- this.dictionary.update((value) => ({ ...value, ...this.labels() }));
132
- }
133
- });
134
- this.value = input.required();
135
- this.editorRef = viewChild.required('imageEditor');
136
- this.ionContent = viewChild.required(IonContent, { read: ElementRef });
137
- this.filters = signal([]);
138
- this.footerMenu = signal('menu');
139
- this.currentCrop = signal('cover');
140
- this.currentRotate = signal(0);
141
- this.photoCrop = signal({
142
- width: 0,
143
- height: 0,
144
- });
145
- this.isCropped = signal(false);
146
- this.adoptFilter = signal(undefined);
147
- this.filterPreset = computed(() => filterPreset(this.dictionary()));
148
- this.footerMenu$ = toObservable(this.footerMenu);
149
- this.initSubscription$ = [];
150
- this.filterImageSize = 240;
151
- this.canvasContainerObserver = new MutationObserver((mutationsList) => {
152
- if (mutationsList.find((mutation) => mutation.type === 'attributes' && mutation.attributeName === 'style')) {
153
- // Cover the image editor with the parent element
154
- const editorRef = this.editorRef();
155
- editorRef.nativeElement.style.minWidth = mutationsList[0].target.parentElement.style.maxWidth;
156
- editorRef.nativeElement.style.minHeight = mutationsList[0].target.parentElement.style.maxHeight;
157
- this.photoCrop.set({
158
- width: mutationsList[0].target.parentElement.querySelector('canvas').width,
159
- height: mutationsList[0].target.parentElement.querySelector('canvas').height,
160
- });
161
- }
162
- });
163
- initializeEditorIcons();
164
- }
165
- ngOnInit() {
166
- this.initSubscription$.push(this.footerMenu$.subscribe((value) => {
167
- if (value === 'filter') {
168
- this.initializeFilterMenu().then();
169
- }
170
- if (value === 'crop') {
171
- this.editorInstance.startDrawingMode('CROPPER');
172
- this.changeCrop(this.requireSquare() ? '1' : 'cover');
173
- }
174
- }));
175
- }
176
- ngOnDestroy() {
177
- this.initSubscription$.forEach((subscription) => subscription.unsubscribe());
178
- }
179
- async ionViewDidEnter() {
180
- this.editorInstance = new ImageEditor(this.editorRef().nativeElement, {
181
- cssMaxWidth: this.ionContent().nativeElement.clientWidth - 32,
182
- cssMaxHeight: this.ionContent().nativeElement.clientHeight - 32,
183
- });
184
- waitToFindDom(this.editorRef().nativeElement, '.tui-image-editor-canvas-container').then(() => {
185
- this.canvasContainerObserver.observe(this.editorRef().nativeElement.querySelector('.tui-image-editor-canvas-container'), {
186
- attributes: true,
187
- childList: false,
188
- subtree: true,
189
- });
190
- });
191
- const blob = await fetch(this.value()).then((res) => res.blob());
192
- await this.editorInstance.loadImageFromFile(new File([blob], 'data.png', { type: blob.type }));
193
- this.footerMenu.set(this.requireSquare() ? 'crop' : 'menu');
194
- }
195
- ionViewDidLeave() {
196
- this.editorInstance.destroy();
197
- this.canvasContainerObserver.disconnect();
198
- }
199
- changeCrop(crop) {
200
- const rect = crop === 'cover' ? this.photoCrop().width / this.photoCrop().height : crop === '16/9' ? 16 / 9 : 1;
201
- this.editorInstance.setCropzoneRect(crop !== 'auto' ? rect : undefined);
202
- this.currentCrop.set(crop);
203
- }
204
- async rotate() {
205
- this.editorInstance.stopDrawingMode();
206
- await this.editorInstance.rotate(90);
207
- this.currentRotate.update((value) => value + 90);
208
- this.editorInstance.startDrawingMode('CROPPER');
209
- requestAnimationFrame(() => this.changeCrop(this.currentCrop()));
210
- }
211
- async closeCrop(type) {
212
- if (this.footerMenu() === 'crop') {
213
- if (type === 'cancel') {
214
- await this.editorInstance.rotate(this.currentRotate() * -1);
215
- }
216
- else {
217
- await this.editorInstance.crop(this.editorInstance.getCropzoneRect());
218
- this.isCropped.set(true);
219
- }
220
- this.currentRotate.set(0);
221
- this.currentCrop.set('cover');
222
- this.editorInstance.stopDrawingMode();
223
- }
224
- else if (this.footerMenu() === 'brightness') {
225
- if (type === 'cancel' && this.editorInstance.hasFilter('brightness')) {
226
- await this.editorInstance.removeFilter('brightness');
227
- }
228
- }
229
- this.footerMenu.set('menu');
230
- }
231
- async changeRange(event) {
232
- if (this.editorInstance.hasFilter('brightness')) {
233
- await this.editorInstance.removeFilter('brightness');
234
- }
235
- await this.editorInstance.applyFilter('brightness', {
236
- brightness: Number(event.detail.value) / 255,
237
- });
238
- }
239
- imageSave() {
240
- const value = this.editorInstance.toDataURL();
241
- this.modalCtrl.dismiss({ value });
242
- }
243
- async initializeFilterMenu() {
244
- const filters = [];
245
- const defaultInstance = new ImageEditor(document.createElement('div'), {
246
- cssMaxWidth: this.filterImageSize,
247
- cssMaxHeight: (this.photoCrop().height * this.filterImageSize) / this.photoCrop().width,
248
- });
249
- const blob = await fetch(this.editorInstance.toDataURL({
250
- multiplier: this.filterImageSize / this.photoCrop().width,
251
- })).then((res) => res.blob());
252
- await defaultInstance.loadImageFromFile(new File([blob], 'defaultInstance.png', { type: blob.type }));
253
- for (const filter of this.filterPreset()) {
254
- if (filter.type !== 'Default') {
255
- await defaultInstance.applyFilter(filter.type, filter.option);
256
- }
257
- filters.push({
258
- name: filter.name,
259
- type: filter.type,
260
- option: filter.option,
261
- data: defaultInstance.toDataURL(),
262
- width: this.filterImageSize,
263
- height: (this.photoCrop().height * this.filterImageSize) / this.photoCrop().width,
264
- });
265
- if (filter.type !== 'Default') {
266
- await defaultInstance.removeFilter(filter.type);
267
- }
268
- }
269
- this.filters.set(filters);
270
- defaultInstance.destroy();
271
- }
272
- async filterImage(filter) {
273
- if (this.adoptFilter()) {
274
- await this.editorInstance.removeFilter(this.adoptFilter().type);
275
- }
276
- if (filter.type === 'Default') {
277
- this.adoptFilter.set(undefined);
278
- return;
279
- }
280
- await this.editorInstance.applyFilter(filter.type, filter.option);
281
- this.adoptFilter.set(filter);
282
- }
283
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PhotoEditorPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
284
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: PhotoEditorPage, isStandalone: true, selector: "app-editor-image", inputs: { requireSquare: { classPropertyName: "requireSquare", publicName: "requireSquare", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["imageEditor"], descendants: true, isSignal: true }, { propertyName: "ionContent", first: true, predicate: IonContent, descendants: true, read: ElementRef, isSignal: true }], ngImport: i0, template: "<ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"start\"\n ><ion-button (click)=\"modalCtrl.dismiss()\"><ion-icon name=\"close-outline\" slot=\"icon-only\"></ion-icon></ion-button\n ></ion-buttons>\n <ion-buttons slot=\"end\">\n <ion-button color=\"photo-editor-primary\" fill=\"outline\" [disabled]=\"!['menu', 'filter'].includes(footerMenu())\" (click)=\"imageSave()\">\n {{ dictionary().save }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\" [scrollY]=\"false\">\n <div #imageEditor></div>\n</ion-content>\n<ion-footer>\n <ion-toolbar>\n @if (this.footerMenu() === 'menu') {\n <aside class=\"ion-justify-content-center\">\n <button (click)=\"footerMenu.set('crop')\">\n <ion-text>{{ dictionary().crop }}</ion-text>\n <ion-icon name=\"crop-outline\" size=\"large\"></ion-icon>\n </button>\n <button (click)=\"footerMenu.set('filter')\">\n <ion-text>{{ dictionary().filter }}</ion-text>\n <ion-icon name=\"color-filter-outline\" size=\"large\"></ion-icon>\n </button>\n <button (click)=\"footerMenu.set('brightness')\">\n <ion-text>{{ dictionary().brightness }}</ion-text>\n <ion-icon name=\"sunny-outline\" size=\"large\"></ion-icon>\n </button>\n </aside>\n }\n\n @if (this.footerMenu() === 'filter') {\n <aside>\n @for (item of filters(); track item) {\n <button (click)=\"filterImage(item)\">\n <ion-text>{{ item.name }}</ion-text>\n <span class=\"image-filter-box\" [style.background-image]=\"'url(' + item.data + ')'\"></span>\n </button>\n }\n </aside>\n }\n\n @if (this.footerMenu() === 'crop' && !requireSquare()) {\n <ion-buttons class=\"ion-justify-content-center submenu-icon-buttons\">\n <ion-button (click)=\"changeCrop('cover')\" [color]=\"currentCrop() === 'cover' ? 'success' : undefined\">\n <ion-icon name=\"expand-outline\" slot=\"icon-only\"></ion-icon>\n </ion-button>\n <ion-button (click)=\"changeCrop('16/9')\" [color]=\"currentCrop() === '16/9' ? 'success' : undefined\">\n <ion-icon name=\"tablet-landscape-outline\" slot=\"icon-only\" style=\"transform: scale(1, 0.8)\"></ion-icon>\n </ion-button>\n <ion-button (click)=\"changeCrop('1')\" [color]=\"currentCrop() === '1' ? 'success' : undefined\">\n <ion-icon name=\"square-outline\" slot=\"icon-only\"></ion-icon>\n </ion-button>\n <ion-button (click)=\"changeCrop('auto')\" [color]=\"currentCrop() === 'auto' ? 'success' : undefined\">\n <ion-icon name=\"crop-outline\" slot=\"icon-only\"></ion-icon>\n </ion-button>\n <ion-button (click)=\"rotate()\"><ion-icon name=\"refresh-outline\" slot=\"icon-only\"></ion-icon></ion-button>\n </ion-buttons>\n }\n\n @if (['brightness'].includes(this.footerMenu())) {\n <div class=\"ion-padding ion-margin\">\n <ion-range [pin]=\"true\" [min]=\"-100\" [max]=\"100\" (ionChange)=\"changeRange($any($event))\"></ion-range>\n </div>\n }\n </ion-toolbar>\n @if (this.footerMenu() !== 'menu') {\n <ion-toolbar mode=\"md\">\n @if (this.footerMenu() === 'filter') {\n <ion-buttons class=\"ion-justify-content-center\" style=\"margin: 0 8px\">\n <ion-button fill=\"outline\" shape=\"round\" (click)=\"this.footerMenu.set('menu')\">\u623B\u308B</ion-button>\n </ion-buttons>\n } @else {\n <ion-buttons slot=\"start\" style=\"min-width: 60px\">\n @if (!requireSquare() || isCropped()) {\n <ion-button (click)=\"closeCrop('cancel')\"><ion-icon name=\"close-outline\" slot=\"icon-only\"></ion-icon></ion-button>\n }</ion-buttons\n ><ion-text class=\"footer-title\">\n @if (footerMenu() === 'crop') {\n {{ dictionary().crop }}\n } @else if (footerMenu() === 'brightness') {\n {{ dictionary().brightness }}\n }</ion-text\n ><ion-buttons slot=\"end\"\n ><ion-button (click)=\"closeCrop('apply')\"\n ><ion-icon name=\"checkmark-outline\" color=\"photo-editor-success\" slot=\"icon-only\"></ion-icon></ion-button\n ></ion-buttons>\n }\n </ion-toolbar>\n }\n</ion-footer>\n", styles: [":host{--editor-background: var(--ion-photo-editor-background, --ion-color-step-100, #2a2a2a);--editor-background-tint: var(--ion-photo-editor-background-tint, --ion-color-step-200, #414141);--editor-color: var(--ion-photo-editor-color, --ion-color-step-950, #f0f0f0);--editor-color-tint: var(--ion-photo-editor-color-tint, --ion-color-step-850, #dbdbdb);--ion-color-photo-editor-primary: var(--ion-photo-editor-primary, --ion-color-primary, #4d8dff);--ion-color-photo-editor-danger: var(--ion-photo-editor-danger, #f24c58);--ion-color-photo-editor-success: var(--ion-photo-editor-success, #2dd55b)}.ion-color-photo-editor-primary{--ion-color-base: var(--ion-color-photo-editor-primary)}.ion-color-photo-editor-danger{--ion-color-base: var(--ion-color-photo-editor-danger)}.ion-color-photo-editor-success{--ion-color-base: var(--ion-color-photo-editor-success)}ion-content,ion-toolbar,ion-header,ion-footer{background:var(--editor-background)!important;--background: var(--editor-background) !important}\n", "ion-content{--background: var(--editor-background)}ion-content>div{width:100%;height:100%;max-width:100%;max-height:100%;display:grid;align-items:center;justify-items:center}aside{display:flex;overflow-x:scroll;margin-top:16px;min-height:120px}aside ion-icon{color:var(--editor-color);border:1px solid var(--editor-background-tint);border-radius:50%;padding:16px}aside button{width:120px;display:block;flex:1;background:transparent;margin:0 8px 16px}aside button ion-text{display:block;color:var(--editor-color);margin-bottom:8px;font-size:.8rem}aside button span.image-filter-box{width:96px;height:64px;border-radius:8px;overflow:hidden;display:block;background-position:center,center;background-size:cover}ion-header ion-button,ion-footer ion-button{--color: var(--editor-color) !important}ion-footer ion-toolbar ion-text.footer-title{color:var(--editor-color)!important;display:block;text-align:center}ion-footer ion-toolbar ion-range{--bar-background: var(--editor-color);--bar-background-active: var(--editor-color);--bar-height: 2px;--bar-border-radius: 8px;--knob-background: var(--editor-color);--knob-size: 20px;--pin-background: none;--pin-color: var(--editor-color)}ion-footer ion-toolbar ion-buttons[slot=start] ion-button,ion-footer ion-toolbar ion-buttons[slot=end] ion-button{--background: var(--editor-background-tint) !important;--border-radius: 50%;margin:0 8px;min-width:32px;min-height:32px}ion-footer ion-toolbar ion-buttons.submenu-icon-buttons ion-button{margin:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: i1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i1.IonRange, selector: "ion-range", inputs: ["activeBarStart", "color", "debounce", "disabled", "dualKnobs", "label", "labelPlacement", "max", "min", "mode", "name", "pin", "pinFormatter", "snaps", "step", "ticks", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
285
- }
286
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PhotoEditorPage, decorators: [{
287
- type: Component,
288
- args: [{ selector: 'app-editor-image', imports: [CommonModule, FormsModule, ...ionComponents], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"start\"\n ><ion-button (click)=\"modalCtrl.dismiss()\"><ion-icon name=\"close-outline\" slot=\"icon-only\"></ion-icon></ion-button\n ></ion-buttons>\n <ion-buttons slot=\"end\">\n <ion-button color=\"photo-editor-primary\" fill=\"outline\" [disabled]=\"!['menu', 'filter'].includes(footerMenu())\" (click)=\"imageSave()\">\n {{ dictionary().save }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\" [scrollY]=\"false\">\n <div #imageEditor></div>\n</ion-content>\n<ion-footer>\n <ion-toolbar>\n @if (this.footerMenu() === 'menu') {\n <aside class=\"ion-justify-content-center\">\n <button (click)=\"footerMenu.set('crop')\">\n <ion-text>{{ dictionary().crop }}</ion-text>\n <ion-icon name=\"crop-outline\" size=\"large\"></ion-icon>\n </button>\n <button (click)=\"footerMenu.set('filter')\">\n <ion-text>{{ dictionary().filter }}</ion-text>\n <ion-icon name=\"color-filter-outline\" size=\"large\"></ion-icon>\n </button>\n <button (click)=\"footerMenu.set('brightness')\">\n <ion-text>{{ dictionary().brightness }}</ion-text>\n <ion-icon name=\"sunny-outline\" size=\"large\"></ion-icon>\n </button>\n </aside>\n }\n\n @if (this.footerMenu() === 'filter') {\n <aside>\n @for (item of filters(); track item) {\n <button (click)=\"filterImage(item)\">\n <ion-text>{{ item.name }}</ion-text>\n <span class=\"image-filter-box\" [style.background-image]=\"'url(' + item.data + ')'\"></span>\n </button>\n }\n </aside>\n }\n\n @if (this.footerMenu() === 'crop' && !requireSquare()) {\n <ion-buttons class=\"ion-justify-content-center submenu-icon-buttons\">\n <ion-button (click)=\"changeCrop('cover')\" [color]=\"currentCrop() === 'cover' ? 'success' : undefined\">\n <ion-icon name=\"expand-outline\" slot=\"icon-only\"></ion-icon>\n </ion-button>\n <ion-button (click)=\"changeCrop('16/9')\" [color]=\"currentCrop() === '16/9' ? 'success' : undefined\">\n <ion-icon name=\"tablet-landscape-outline\" slot=\"icon-only\" style=\"transform: scale(1, 0.8)\"></ion-icon>\n </ion-button>\n <ion-button (click)=\"changeCrop('1')\" [color]=\"currentCrop() === '1' ? 'success' : undefined\">\n <ion-icon name=\"square-outline\" slot=\"icon-only\"></ion-icon>\n </ion-button>\n <ion-button (click)=\"changeCrop('auto')\" [color]=\"currentCrop() === 'auto' ? 'success' : undefined\">\n <ion-icon name=\"crop-outline\" slot=\"icon-only\"></ion-icon>\n </ion-button>\n <ion-button (click)=\"rotate()\"><ion-icon name=\"refresh-outline\" slot=\"icon-only\"></ion-icon></ion-button>\n </ion-buttons>\n }\n\n @if (['brightness'].includes(this.footerMenu())) {\n <div class=\"ion-padding ion-margin\">\n <ion-range [pin]=\"true\" [min]=\"-100\" [max]=\"100\" (ionChange)=\"changeRange($any($event))\"></ion-range>\n </div>\n }\n </ion-toolbar>\n @if (this.footerMenu() !== 'menu') {\n <ion-toolbar mode=\"md\">\n @if (this.footerMenu() === 'filter') {\n <ion-buttons class=\"ion-justify-content-center\" style=\"margin: 0 8px\">\n <ion-button fill=\"outline\" shape=\"round\" (click)=\"this.footerMenu.set('menu')\">\u623B\u308B</ion-button>\n </ion-buttons>\n } @else {\n <ion-buttons slot=\"start\" style=\"min-width: 60px\">\n @if (!requireSquare() || isCropped()) {\n <ion-button (click)=\"closeCrop('cancel')\"><ion-icon name=\"close-outline\" slot=\"icon-only\"></ion-icon></ion-button>\n }</ion-buttons\n ><ion-text class=\"footer-title\">\n @if (footerMenu() === 'crop') {\n {{ dictionary().crop }}\n } @else if (footerMenu() === 'brightness') {\n {{ dictionary().brightness }}\n }</ion-text\n ><ion-buttons slot=\"end\"\n ><ion-button (click)=\"closeCrop('apply')\"\n ><ion-icon name=\"checkmark-outline\" color=\"photo-editor-success\" slot=\"icon-only\"></ion-icon></ion-button\n ></ion-buttons>\n }\n </ion-toolbar>\n }\n</ion-footer>\n", styles: [":host{--editor-background: var(--ion-photo-editor-background, --ion-color-step-100, #2a2a2a);--editor-background-tint: var(--ion-photo-editor-background-tint, --ion-color-step-200, #414141);--editor-color: var(--ion-photo-editor-color, --ion-color-step-950, #f0f0f0);--editor-color-tint: var(--ion-photo-editor-color-tint, --ion-color-step-850, #dbdbdb);--ion-color-photo-editor-primary: var(--ion-photo-editor-primary, --ion-color-primary, #4d8dff);--ion-color-photo-editor-danger: var(--ion-photo-editor-danger, #f24c58);--ion-color-photo-editor-success: var(--ion-photo-editor-success, #2dd55b)}.ion-color-photo-editor-primary{--ion-color-base: var(--ion-color-photo-editor-primary)}.ion-color-photo-editor-danger{--ion-color-base: var(--ion-color-photo-editor-danger)}.ion-color-photo-editor-success{--ion-color-base: var(--ion-color-photo-editor-success)}ion-content,ion-toolbar,ion-header,ion-footer{background:var(--editor-background)!important;--background: var(--editor-background) !important}\n", "ion-content{--background: var(--editor-background)}ion-content>div{width:100%;height:100%;max-width:100%;max-height:100%;display:grid;align-items:center;justify-items:center}aside{display:flex;overflow-x:scroll;margin-top:16px;min-height:120px}aside ion-icon{color:var(--editor-color);border:1px solid var(--editor-background-tint);border-radius:50%;padding:16px}aside button{width:120px;display:block;flex:1;background:transparent;margin:0 8px 16px}aside button ion-text{display:block;color:var(--editor-color);margin-bottom:8px;font-size:.8rem}aside button span.image-filter-box{width:96px;height:64px;border-radius:8px;overflow:hidden;display:block;background-position:center,center;background-size:cover}ion-header ion-button,ion-footer ion-button{--color: var(--editor-color) !important}ion-footer ion-toolbar ion-text.footer-title{color:var(--editor-color)!important;display:block;text-align:center}ion-footer ion-toolbar ion-range{--bar-background: var(--editor-color);--bar-background-active: var(--editor-color);--bar-height: 2px;--bar-border-radius: 8px;--knob-background: var(--editor-color);--knob-size: 20px;--pin-background: none;--pin-color: var(--editor-color)}ion-footer ion-toolbar ion-buttons[slot=start] ion-button,ion-footer ion-toolbar ion-buttons[slot=end] ion-button{--background: var(--editor-background-tint) !important;--border-radius: 50%;margin:0 8px;min-width:32px;min-height:32px}ion-footer ion-toolbar ion-buttons.submenu-icon-buttons ion-button{margin:16px}\n"] }]
289
- }], ctorParameters: () => [] });
290
-
291
- class PhotoViewerPage {
292
- constructor() {
293
- this.imageUrls = input.required();
294
- this.index = input(0, {
295
- transform: coerceNumberProperty,
296
- });
297
- this.isCircle = input(false, {
298
- transform: coerceBooleanProperty,
299
- });
300
- this.enableDelete = input(false, {
301
- transform: coerceBooleanProperty,
302
- });
303
- this.enableFooterSafeArea = input(false, {
304
- transform: coerceBooleanProperty,
305
- });
306
- this.labels = input();
307
- this.setLabels = effect(() => {
308
- if (this.labels()) {
309
- this.dictionary.update((value) => ({ ...value, ...this.labels() }));
310
- }
311
- });
312
- this.swiper = viewChild.required('swiper');
313
- this.dictionary = signal(dictionaryForViewer());
314
- this.watchSwipe$ = new Subscription();
315
- this.modalCtrl = inject(ModalController);
316
- this.el = inject(ElementRef);
317
- register();
318
- initializeViewerIcons();
319
- }
320
- async ngOnInit() {
321
- waitToFindDom(this.el.nativeElement, 'swiper-container').then(() => {
322
- const index = this.index();
323
- const swiper = this.swiper();
324
- Object.assign(swiper.nativeElement, {
325
- modules: [Navigation, Zoom, IonicSlides],
326
- initialSlide: index,
327
- slidesPerView: 1,
328
- pagination: {
329
- enabled: true,
330
- clickable: true,
331
- },
332
- zoom: true,
333
- });
334
- swiper.nativeElement.initialize();
335
- swiper.nativeElement.swiper.zoom.enable();
336
- swiper.nativeElement.swiper.activeIndex = index;
337
- swiper.nativeElement.swiper.update();
338
- });
339
- this.watchSwipe$.add(fromEvent(this.el.nativeElement, 'touchstart')
340
- .pipe(zipWith(fromEvent(this.el.nativeElement, 'touchend').pipe(withLatestFrom(fromEvent(this.el.nativeElement, 'touchmove')))), throttleTime(1))
341
- .subscribe(([touchstart, [_, touchmove]]) => {
342
- const touchstartClientX = touchstart.touches ? touchstart.touches[0].clientX : touchstart.detail[1].clientX;
343
- const touchmoveClientX = touchmove.touches ? touchmove.touches[0].clientX : touchmove.detail[1].clientX;
344
- const touchstartClientY = touchstart.touches ? touchstart.touches[0].clientY : touchstart.detail[1].clientY;
345
- const touchmoveClientY = touchmove.touches ? touchmove.touches[0].clientY : touchmove.detail[1].clientY;
346
- const xDiff = touchstartClientX - touchmoveClientX;
347
- const yDiff = touchstartClientY - touchmoveClientY;
348
- const slides = this.swiper().nativeElement.querySelectorAll('swiper-slide');
349
- const isZoomed = Array.from(slides).find((slide) => {
350
- return ['swiper-slide-zoomed', 'swiper-slide-active'].every((c) => slide.classList.contains(c));
351
- });
352
- const threshold = touchmove.touches ? -50 : -5;
353
- if (!isZoomed && Math.abs(xDiff) < Math.abs(threshold) && yDiff < threshold && touchstart.timeStamp <= touchmove.timeStamp) {
354
- this.watchSwipe$.unsubscribe();
355
- this.modalCtrl.dismiss();
356
- }
357
- }));
358
- }
359
- ngOnDestroy() {
360
- this.watchSwipe$.unsubscribe();
361
- }
362
- remove() {
363
- this.modalCtrl.dismiss({
364
- delete: {
365
- index: this.swiper().nativeElement.swiper.activeIndex,
366
- value: this.imageUrls()[this.swiper().nativeElement.swiper.activeIndex],
367
- },
368
- });
369
- }
370
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PhotoViewerPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
371
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: PhotoViewerPage, isStandalone: true, selector: "app-photo-image", inputs: { imageUrls: { classPropertyName: "imageUrls", publicName: "imageUrls", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, isCircle: { classPropertyName: "isCircle", publicName: "isCircle", isSignal: true, isRequired: false, transformFunction: null }, enableDelete: { classPropertyName: "enableDelete", publicName: "enableDelete", isSignal: true, isRequired: false, transformFunction: null }, enableFooterSafeArea: { classPropertyName: "enableFooterSafeArea", publicName: "enableFooterSafeArea", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "swiper", first: true, predicate: ["swiper"], descendants: true, isSignal: true }], ngImport: i0, template: "<ion-header [translucent]=\"true\">\n <ion-toolbar>\n <ion-buttons slot=\"start\">\n <ion-button (click)=\"modalCtrl.dismiss()\" class=\"viewer-close-button\">\n <ion-icon name=\"close-outline\" slot=\"icon-only\"></ion-icon>\n </ion-button>\n </ion-buttons>\n @if (enableDelete()) {\n <ion-buttons slot=\"end\">\n <ion-button color=\"photo-editor-danger\" fill=\"outline\" (click)=\"remove()\">{{ dictionary().delete }}</ion-button>\n </ion-buttons>\n }\n </ion-toolbar>\n</ion-header>\n\n<ion-content [fullscreen]=\"true\">\n <swiper-container #swiper>\n @for (url of imageUrls(); track url) {\n <swiper-slide>\n <div class=\"swiper-zoom-container\"><img [src]=\"url\" alt=\"\" [class.circle]=\"isCircle()\" /></div>\n </swiper-slide>\n }\n </swiper-container>\n</ion-content>\n\n@if (enableFooterSafeArea()) {\n <!-- use for safe area-->\n <ion-footer>\n <ion-toolbar style=\"--border-width: 0; --min-height: 0\"></ion-toolbar>\n </ion-footer>\n}\n", styles: [":host{--editor-background: var(--ion-photo-editor-background, --ion-color-step-100, #2a2a2a);--editor-background-tint: var(--ion-photo-editor-background-tint, --ion-color-step-200, #414141);--editor-color: var(--ion-photo-editor-color, --ion-color-step-950, #f0f0f0);--editor-color-tint: var(--ion-photo-editor-color-tint, --ion-color-step-850, #dbdbdb);--ion-color-photo-editor-primary: var(--ion-photo-editor-primary, --ion-color-primary, #4d8dff);--ion-color-photo-editor-danger: var(--ion-photo-editor-danger, #f24c58);--ion-color-photo-editor-success: var(--ion-photo-editor-success, #2dd55b)}.ion-color-photo-editor-primary{--ion-color-base: var(--ion-color-photo-editor-primary)}.ion-color-photo-editor-danger{--ion-color-base: var(--ion-color-photo-editor-danger)}.ion-color-photo-editor-success{--ion-color-base: var(--ion-color-photo-editor-success)}ion-content,ion-toolbar,ion-header,ion-footer{background:var(--editor-background)!important;--background: var(--editor-background) !important}\n", "ion-button.viewer-close-button{--color: var(--editor-color) !important}img.circle{border-radius:50%;display:block;padding:16px}swiper-container{height:100%;max-height:100%}swiper-slide{height:100%;max-height:100%;display:flex;justify-content:center;align-items:center}swiper-slide img{display:block;object-fit:contain;max-width:100%;max-height:100%}\n"], dependencies: [{ kind: "component", type: i1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
372
- }
373
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PhotoViewerPage, decorators: [{
374
- type: Component,
375
- args: [{ selector: 'app-photo-image', imports: [...ionComponents], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ion-header [translucent]=\"true\">\n <ion-toolbar>\n <ion-buttons slot=\"start\">\n <ion-button (click)=\"modalCtrl.dismiss()\" class=\"viewer-close-button\">\n <ion-icon name=\"close-outline\" slot=\"icon-only\"></ion-icon>\n </ion-button>\n </ion-buttons>\n @if (enableDelete()) {\n <ion-buttons slot=\"end\">\n <ion-button color=\"photo-editor-danger\" fill=\"outline\" (click)=\"remove()\">{{ dictionary().delete }}</ion-button>\n </ion-buttons>\n }\n </ion-toolbar>\n</ion-header>\n\n<ion-content [fullscreen]=\"true\">\n <swiper-container #swiper>\n @for (url of imageUrls(); track url) {\n <swiper-slide>\n <div class=\"swiper-zoom-container\"><img [src]=\"url\" alt=\"\" [class.circle]=\"isCircle()\" /></div>\n </swiper-slide>\n }\n </swiper-container>\n</ion-content>\n\n@if (enableFooterSafeArea()) {\n <!-- use for safe area-->\n <ion-footer>\n <ion-toolbar style=\"--border-width: 0; --min-height: 0\"></ion-toolbar>\n </ion-footer>\n}\n", styles: [":host{--editor-background: var(--ion-photo-editor-background, --ion-color-step-100, #2a2a2a);--editor-background-tint: var(--ion-photo-editor-background-tint, --ion-color-step-200, #414141);--editor-color: var(--ion-photo-editor-color, --ion-color-step-950, #f0f0f0);--editor-color-tint: var(--ion-photo-editor-color-tint, --ion-color-step-850, #dbdbdb);--ion-color-photo-editor-primary: var(--ion-photo-editor-primary, --ion-color-primary, #4d8dff);--ion-color-photo-editor-danger: var(--ion-photo-editor-danger, #f24c58);--ion-color-photo-editor-success: var(--ion-photo-editor-success, #2dd55b)}.ion-color-photo-editor-primary{--ion-color-base: var(--ion-color-photo-editor-primary)}.ion-color-photo-editor-danger{--ion-color-base: var(--ion-color-photo-editor-danger)}.ion-color-photo-editor-success{--ion-color-base: var(--ion-color-photo-editor-success)}ion-content,ion-toolbar,ion-header,ion-footer{background:var(--editor-background)!important;--background: var(--editor-background) !important}\n", "ion-button.viewer-close-button{--color: var(--editor-color) !important}img.circle{border-radius:50%;display:block;padding:16px}swiper-container{height:100%;max-height:100%}swiper-slide{height:100%;max-height:100%;display:flex;justify-content:center;align-items:center}swiper-slide img{display:block;object-fit:contain;max-width:100%;max-height:100%}\n"] }]
376
- }], ctorParameters: () => [] });
377
-
378
- var PhotoEditorErrors;
379
- (function (PhotoEditorErrors) {
380
- PhotoEditorErrors["initialize"] = "input dom is not found";
381
- PhotoEditorErrors["cancel"] = "user canceled";
382
- PhotoEditorErrors["type"] = "upload file is not image.";
383
- })(PhotoEditorErrors || (PhotoEditorErrors = {}));
384
-
385
- class PhotoFileService {
386
- constructor() {
387
- this.$photoMaxSize = signal(1000);
388
- this.actionSheetCtrl = inject(ActionSheetController);
389
- this.platform = inject(Platform);
390
- this.dictionary = signal(dictionaryForService());
391
- }
392
- set photoMaxSize(value) {
393
- this.$photoMaxSize.set(value);
394
- }
395
- set labels(d) {
396
- this.dictionary.update((value) => ({ ...value, ...d }));
397
- }
398
- async loadPhoto(limit) {
399
- /**
400
- * Using Input for browser
401
- */
402
- if (!this.platform.is('capacitor')) {
403
- return this.getPictureFromBrowser();
404
- }
405
- const actionSheet = await this.actionSheetCtrl.create({
406
- buttons: [
407
- {
408
- text: this.dictionary().camera,
409
- handler: () => {
410
- actionSheet.dismiss('camera');
411
- },
412
- },
413
- {
414
- text: this.dictionary().album,
415
- handler: () => {
416
- actionSheet.dismiss('album');
417
- },
418
- },
419
- {
420
- text: this.dictionary().cancel,
421
- role: 'cancel',
422
- },
423
- ],
424
- });
425
- await actionSheet.present();
426
- const { data } = await actionSheet.onDidDismiss();
427
- if (!data) {
428
- return Promise.reject(PhotoEditorErrors.cancel);
429
- }
430
- if (data === 'camera') {
431
- const defaultCamera = {
432
- quality: 100,
433
- width: this.$photoMaxSize(),
434
- allowEditing: false,
435
- resultType: CameraResultType.DataUrl,
436
- source: CameraSource.Camera,
437
- presentationStyle: 'popover',
438
- };
439
- const image = await Camera.getPhoto(defaultCamera).catch(() => undefined);
440
- if (!image?.dataUrl) {
441
- return Promise.reject(PhotoEditorErrors.cancel);
442
- }
443
- if (!image.dataUrl.includes('capacitor://localhost')) {
444
- return [image.dataUrl];
445
- }
446
- return Promise.all([image.dataUrl].map(async (image) => await this.loadPhotoFromFilePath(image)));
447
- }
448
- if (data === 'album') {
449
- const images = await Camera.pickImages({
450
- quality: 100,
451
- width: this.$photoMaxSize(),
452
- limit,
453
- presentationStyle: 'popover',
454
- }).catch(() => undefined);
455
- if (!images) {
456
- return Promise.reject(PhotoEditorErrors.cancel);
457
- }
458
- return Promise.all(images.photos.map(async (image) => await this.loadPhotoFromFilePath(image.webPath)));
459
- }
460
- // Not run on this line. This is for lint
461
- return [];
462
- }
463
- getPictureFromBrowser() {
464
- const inputFile = document.querySelector('input#browserPhotoUploader');
465
- if (!inputFile) {
466
- return Promise.reject(PhotoEditorErrors.initialize);
467
- }
468
- return new Promise((resolve, reject) => {
469
- const cancelMethod = () => {
470
- inputFile.removeEventListener('cancel', cancelMethod, false);
471
- inputFile.removeEventListener('change', changeMethod, false);
472
- reject(PhotoEditorErrors.cancel);
473
- };
474
- const changeMethod = (e) => {
475
- inputFile.removeEventListener('cancel', cancelMethod, false);
476
- inputFile.removeEventListener('change', changeMethod, false);
477
- if (!e.target.files || !e.target.files[0]) {
478
- reject(PhotoEditorErrors.cancel);
479
- }
480
- const file = e.target.files[0];
481
- const reader = new FileReader();
482
- reader.onload = (() => {
483
- if (file.type.indexOf('image') < 0) {
484
- reject(PhotoEditorErrors.type);
485
- }
486
- return async (event) => {
487
- inputFile.value = '';
488
- const result = event.target.result;
489
- const data = await this.loadPhotoFromFilePath(result);
490
- resolve([data]);
491
- };
492
- })();
493
- reader.readAsDataURL(file);
494
- };
495
- inputFile.addEventListener('cancel', cancelMethod, false);
496
- inputFile.addEventListener('change', changeMethod, false);
497
- inputFile.click();
498
- });
499
- }
500
- async loadPhotoFromFilePath(filePath) {
501
- const defaultInstance = new ImageEditor(document.createElement('div'), {
502
- cssMaxWidth: this.$photoMaxSize(),
503
- cssMaxHeight: this.$photoMaxSize(),
504
- });
505
- const blob = await fetch(filePath).then((res) => res.blob());
506
- const loaded = await defaultInstance.loadImageFromFile(new File([blob], 'data.png', { type: blob.type }));
507
- const maxSize = Math.max(loaded.newWidth, loaded.newHeight);
508
- const dataUrl = defaultInstance.toDataURL({
509
- multiplier: this.$photoMaxSize() / maxSize,
510
- });
511
- defaultInstance.destroy();
512
- return dataUrl;
513
- }
514
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PhotoFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
515
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PhotoFileService, providedIn: 'root' }); }
516
- }
517
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: PhotoFileService, decorators: [{
518
- type: Injectable,
519
- args: [{
520
- providedIn: 'root',
521
- }]
522
- }] });
523
-
524
- /*
525
- * Public API Surface of photo-editor
526
- */
527
-
528
- /**
529
- * Generated bundle index. Do not edit.
530
- */
531
-
532
- export { PhotoEditorPage, PhotoFileService, PhotoViewerPage };
533
- //# sourceMappingURL=rdlabo-ionic-angular-photo-editor.mjs.map