@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.
@@ -0,0 +1,7 @@
1
+ // @ts-check
2
+ const tseslint = require("typescript-eslint");
3
+ const rootConfig = require("../../eslint.config.js");
4
+
5
+ module.exports = tseslint.config(
6
+ ...rootConfig,
7
+ );
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/photo-editor",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-angular-photo-editor",
3
- "version": "20.0.3",
3
+ "version": "21.0.1",
4
4
  "peerDependencies": {
5
- "@angular/cdk": "^20.0.0",
6
- "@angular/common": "^20.0.0",
7
- "@angular/core": "^20.0.0",
5
+ "@angular/cdk": "^21.0.0",
6
+ "@angular/common": "^21.0.0",
7
+ "@angular/core": "^21.0.0",
8
8
  "@capacitor/camera": ">=6.0.0 <8.0.0",
9
9
  "@capacitor/core": ">=6.0.0 <8.0.0",
10
10
  "@ionic/angular": "^8.0.0",
@@ -14,16 +14,5 @@
14
14
  "dependencies": {
15
15
  "tslib": "^2.3.0"
16
16
  },
17
- "sideEffects": false,
18
- "module": "fesm2022/rdlabo-ionic-angular-photo-editor.mjs",
19
- "typings": "index.d.ts",
20
- "exports": {
21
- "./package.json": {
22
- "default": "./package.json"
23
- },
24
- ".": {
25
- "types": "./index.d.ts",
26
- "default": "./fesm2022/rdlabo-ionic-angular-photo-editor.mjs"
27
- }
28
- }
29
- }
17
+ "sideEffects": false
18
+ }
@@ -0,0 +1,31 @@
1
+ import { IDictionaryForEditor, IDictionaryForService, IDictionaryForViewer } from './types';
2
+
3
+ export const dictionaryForEditor = (): IDictionaryForEditor => ({
4
+ // UI labels
5
+ save: '保存',
6
+ crop: '切り抜き・回転',
7
+ filter: 'フィルター',
8
+ brightness: '明るさ',
9
+
10
+ // Filter labels
11
+ original: 'オリジナル',
12
+ invert: '反転',
13
+ sepia: 'セピア',
14
+ vintage: 'ヴィンテージ',
15
+ blur: 'ぼかし',
16
+ grayscale: 'グレースケール',
17
+ sharpen: '輪郭',
18
+ emboss: 'エンボス',
19
+ });
20
+
21
+ export const dictionaryForViewer = (): IDictionaryForViewer => ({
22
+ // UI labels
23
+ delete: '削除',
24
+ });
25
+
26
+ export const dictionaryForService = (): IDictionaryForService => ({
27
+ // UI labels
28
+ camera: 'カメラ撮影',
29
+ album: 'アルバムから選択',
30
+ cancel: 'キャンセル',
31
+ });
@@ -0,0 +1,44 @@
1
+ import { IDictionaryForEditor, IFilterPreset } from './types';
2
+
3
+ export const filterPreset = (dictionary: IDictionaryForEditor): IFilterPreset[] => [
4
+ {
5
+ name: dictionary.original,
6
+ type: 'Default',
7
+ option: null,
8
+ },
9
+ // {
10
+ // name: dictionary.invert,
11
+ // type: 'Invert',
12
+ // option: null,
13
+ // },
14
+ {
15
+ name: dictionary.sepia,
16
+ type: 'Sepia',
17
+ option: null,
18
+ },
19
+ {
20
+ name: dictionary.vintage,
21
+ type: 'vintage',
22
+ option: null,
23
+ },
24
+ {
25
+ name: 'ぼかし',
26
+ type: 'Blur',
27
+ option: { blur: 0.1 },
28
+ },
29
+ {
30
+ name: dictionary.grayscale,
31
+ type: 'Grayscale',
32
+ option: null,
33
+ },
34
+ // {
35
+ // name: dictionary.sharpen,
36
+ // type: 'Sharpen',
37
+ // option: null,
38
+ // },
39
+ // {
40
+ // name: dictionary.emboss,
41
+ // type: 'Emboss',
42
+ // option: null,
43
+ // },
44
+ ];
@@ -0,0 +1,3 @@
1
+ import { IonButton, IonButtons, IonContent, IonFooter, IonHeader, IonIcon, IonRange, IonText, IonToolbar } from '@ionic/angular/standalone';
2
+
3
+ export const ionComponents = [IonHeader, IonToolbar, IonButtons, IonButton, IonIcon, IonContent, IonFooter, IonText, IonRange];
@@ -0,0 +1,63 @@
1
+ :host {
2
+ --editor-background: var(--ion-photo-editor-background, --ion-color-step-100, #2a2a2a);
3
+ --editor-background-tint: var(--ion-photo-editor-background-tint, --ion-color-step-200, #414141);
4
+
5
+ --editor-color: var(--ion-photo-editor-color, --ion-color-step-950, #f0f0f0);
6
+ --editor-color-tint: var(--ion-photo-editor-color-tint, --ion-color-step-850, #dbdbdb);
7
+
8
+ --ion-color-photo-editor-primary: var(--ion-photo-editor-primary, --ion-color-primary, #4d8dff);
9
+ //--ion-color-photo-editor-primary-rgb: var(--ion-photo-editor-primary-rgb, --ion-color-primary-rgb, '77, 141, 255');
10
+ //--ion-color-photo-editor-primary-contrast: var(--ion-photo-editor-primary-contrast, --ion-color-primary-contrast, #000);
11
+ //--ion-color-photo-editor-primary-contrast-rgb: var(--ion-photo-editor-primary-contrast-rgb, --ion-color-primary-rgb, '0,0,0');
12
+ //--ion-color-photo-editor-primary-shade: var(--ion-photo-editor-primary-shade --ion-color-primary-shade, #447ce0);
13
+ //--ion-color-photo-editor-primary-tint: var(--ion-photo-editor-primary-tint, --ion-color-primary-tint, #5f98ff);
14
+
15
+ --ion-color-photo-editor-danger: var(--ion-photo-editor-danger, #f24c58);
16
+ //--ion-color-photo-editor-danger-rgb: var(--ion-photo-editor-danger, '242,76,88');
17
+ //--ion-color-photo-editor-danger-contrast: var(--ion-photo-editor-danger, #000000);
18
+ //--ion-color-photo-editor-danger-contrast-rgb: var(--ion-photo-editor-danger, '0,0,0');
19
+ //--ion-color-photo-editor-danger-shade: var(--ion-photo-editor-danger, #d5434d);
20
+ //--ion-color-photo-editor-danger-tint: var(--ion-photo-editor-danger, #f35e69);
21
+
22
+ --ion-color-photo-editor-success: var(--ion-photo-editor-success, #2dd55b);
23
+ //--ion-color-photo-editor-success-rgb: var(--ion-photo-editor-success, '45,213,91');
24
+ //--ion-color-photo-editor-success-contrast: var(--ion-photo-editor-success, #000000);
25
+ //--ion-color-photo-editor-success-contrast-rgb: var(--ion-photo-editor-success, '0,0,0');
26
+ //--ion-color-photo-editor-success-shade: var(--ion-photo-editor-success, #28bb50);
27
+ //--ion-color-photo-editor-success-tint: var(--ion-photo-editor-success, #42d96b);
28
+ }
29
+
30
+ .ion-color-photo-editor-primary {
31
+ --ion-color-base: var(--ion-color-photo-editor-primary);
32
+ //--ion-color-base-rgb: var(--ion-color-photo-editor-primary-rgb);
33
+ //--ion-color-contrast: var(--ion-color-photo-editor-primary-contrast);
34
+ //--ion-color-contrast-rgb: var(--ion-color-photo-editor-primary-contrast-rgb);
35
+ //--ion-color-shade: var(--ion-color-photo-editor-primary-shade);
36
+ //--ion-color-tint: var(--ion-color-photo-editor-primary-tint);
37
+ }
38
+
39
+ .ion-color-photo-editor-danger {
40
+ --ion-color-base: var(--ion-color-photo-editor-danger);
41
+ //--ion-color-base-rgb: var(--ion-color-photo-editor-danger-rgb);
42
+ //--ion-color-contrast: var(--ion-color-photo-editor-danger-contrast);
43
+ //--ion-color-contrast-rgb: var(--ion-color-photo-editor-danger-contrast-rgb);
44
+ //--ion-color-shade: var(--ion-color-photo-editor-danger-shade);
45
+ //--ion-color-tint: var(--ion-color-photo-editor-danger-tint);
46
+ }
47
+
48
+ .ion-color-photo-editor-success {
49
+ --ion-color-base: var(--ion-color-photo-editor-success);
50
+ //--ion-color-base-rgb: var(--ion-color-photo-editor-success-rgb);
51
+ //--ion-color-contrast: var(--ion-color-photo-editor-success-contrast);
52
+ //--ion-color-contrast-rgb: var(--ion-color-photo-editor-success-contrast-rgb);
53
+ //--ion-color-shade: var(--ion-color-photo-editor-success-shade);
54
+ //--ion-color-tint: var(--ion-color-photo-editor-success-tint);
55
+ }
56
+
57
+ ion-content,
58
+ ion-toolbar,
59
+ ion-header,
60
+ ion-footer {
61
+ background: var(--editor-background) !important;
62
+ --background: var(--editor-background) !important;
63
+ }
@@ -0,0 +1,95 @@
1
+ <ion-header>
2
+ <ion-toolbar>
3
+ <ion-buttons slot="start"
4
+ ><ion-button (click)="modalCtrl.dismiss()"><ion-icon name="close-outline" slot="icon-only"></ion-icon></ion-button
5
+ ></ion-buttons>
6
+ <ion-buttons slot="end">
7
+ <ion-button color="photo-editor-primary" fill="solid" type="submit" [disabled]="!['menu', 'filter'].includes(footerMenu())" (click)="imageSave()">
8
+ {{ dictionary().save }}
9
+ </ion-button>
10
+ </ion-buttons>
11
+ </ion-toolbar>
12
+ </ion-header>
13
+
14
+ <ion-content class="ion-padding" [scrollY]="false">
15
+ <div #imageEditor></div>
16
+ </ion-content>
17
+ <ion-footer>
18
+ <ion-toolbar>
19
+ @if (this.footerMenu() === 'menu') {
20
+ <aside class="ion-justify-content-center">
21
+ <button (click)="footerMenu.set('crop')">
22
+ <ion-text>{{ dictionary().crop }}</ion-text>
23
+ <ion-icon name="crop-outline" size="large"></ion-icon>
24
+ </button>
25
+ <button (click)="footerMenu.set('filter')">
26
+ <ion-text>{{ dictionary().filter }}</ion-text>
27
+ <ion-icon name="color-filter-outline" size="large"></ion-icon>
28
+ </button>
29
+ <button (click)="footerMenu.set('brightness')">
30
+ <ion-text>{{ dictionary().brightness }}</ion-text>
31
+ <ion-icon name="sunny-outline" size="large"></ion-icon>
32
+ </button>
33
+ </aside>
34
+ }
35
+
36
+ @if (this.footerMenu() === 'filter') {
37
+ <aside>
38
+ @for (item of filters(); track item) {
39
+ <button (click)="filterImage(item)">
40
+ <ion-text>{{ item.name }}</ion-text>
41
+ <span class="image-filter-box" [style.background-image]="'url(' + item.data + ')'"></span>
42
+ </button>
43
+ }
44
+ </aside>
45
+ }
46
+
47
+ @if (this.footerMenu() === 'crop' && !requireSquare()) {
48
+ <ion-buttons class="ion-justify-content-center submenu-icon-buttons">
49
+ <ion-button (click)="changeCrop('cover')" [color]="currentCrop() === 'cover' ? 'success' : undefined">
50
+ <ion-icon name="expand-outline" slot="icon-only"></ion-icon>
51
+ </ion-button>
52
+ <ion-button (click)="changeCrop('16/9')" [color]="currentCrop() === '16/9' ? 'success' : undefined">
53
+ <ion-icon name="tablet-landscape-outline" slot="icon-only" style="transform: scale(1, 0.8)"></ion-icon>
54
+ </ion-button>
55
+ <ion-button (click)="changeCrop('1')" [color]="currentCrop() === '1' ? 'success' : undefined">
56
+ <ion-icon name="square-outline" slot="icon-only"></ion-icon>
57
+ </ion-button>
58
+ <ion-button (click)="changeCrop('auto')" [color]="currentCrop() === 'auto' ? 'success' : undefined">
59
+ <ion-icon name="crop-outline" slot="icon-only"></ion-icon>
60
+ </ion-button>
61
+ <ion-button (click)="rotate()"><ion-icon name="refresh-outline" slot="icon-only"></ion-icon></ion-button>
62
+ </ion-buttons>
63
+ }
64
+
65
+ @if (['brightness'].includes(this.footerMenu())) {
66
+ <div class="ion-padding ion-margin">
67
+ <ion-range [pin]="true" [min]="-100" [max]="100" (ionChange)="changeRange($any($event))"></ion-range>
68
+ </div>
69
+ }
70
+ </ion-toolbar>
71
+ @if (this.footerMenu() !== 'menu') {
72
+ <ion-toolbar mode="md">
73
+ @if (this.footerMenu() === 'filter') {
74
+ <ion-buttons class="ion-justify-content-center" style="margin: 0 8px">
75
+ <ion-button fill="outline" shape="round" (click)="this.footerMenu.set('menu')">戻る</ion-button>
76
+ </ion-buttons>
77
+ } @else {
78
+ <ion-buttons slot="start" style="min-width: 60px">
79
+ @if (!requireSquare() || isCropped()) {
80
+ <ion-button (click)="closeCrop('cancel')"><ion-icon name="close-outline" slot="icon-only"></ion-icon></ion-button>
81
+ }</ion-buttons
82
+ ><ion-text class="footer-title">
83
+ @if (footerMenu() === 'crop') {
84
+ {{ dictionary().crop }}
85
+ } @else if (footerMenu() === 'brightness') {
86
+ {{ dictionary().brightness }}
87
+ }</ion-text
88
+ ><ion-buttons slot="end"
89
+ ><ion-button (click)="closeCrop('apply')"
90
+ ><ion-icon name="checkmark-outline" color="photo-editor-success" slot="icon-only"></ion-icon></ion-button
91
+ ></ion-buttons>
92
+ }
93
+ </ion-toolbar>
94
+ }
95
+ </ion-footer>
@@ -0,0 +1,95 @@
1
+ ion-content {
2
+ --background: var(--editor-background);
3
+ & > div {
4
+ width: 100%;
5
+ height: 100%;
6
+ max-width: 100%;
7
+ max-height: 100%;
8
+ display: grid;
9
+ align-items: center;
10
+ justify-items: center;
11
+ }
12
+ }
13
+
14
+ aside {
15
+ display: flex;
16
+ overflow-x: scroll;
17
+ margin-top: 16px;
18
+ min-height: 120px;
19
+
20
+ ion-icon {
21
+ color: var(--editor-color);
22
+ border: 1px solid var(--editor-background-tint);
23
+ border-radius: 50%;
24
+ padding: 16px;
25
+ }
26
+
27
+ button {
28
+ width: 120px;
29
+ display: block;
30
+ flex: 1;
31
+ background: transparent;
32
+ margin: 0 8px 16px;
33
+
34
+ ion-text {
35
+ display: block;
36
+ color: var(--editor-color);
37
+ margin-bottom: 8px;
38
+ font-size: 0.8rem;
39
+ }
40
+ span.image-filter-box {
41
+ width: 96px;
42
+ height: 64px;
43
+ border-radius: 8px;
44
+ overflow: hidden;
45
+ display: block;
46
+ background-position: center, center;
47
+ background-size: cover;
48
+ }
49
+ }
50
+ }
51
+
52
+ ion-header,
53
+ ion-footer {
54
+ ion-button {
55
+ --color: var(--editor-color) !important;
56
+ }
57
+ }
58
+
59
+ ion-footer {
60
+ ion-toolbar {
61
+ ion-text.footer-title {
62
+ color: var(--editor-color) !important;
63
+ display: block;
64
+ text-align: center;
65
+ }
66
+ ion-range {
67
+ --bar-background: var(--editor-color);
68
+ --bar-background-active: var(--editor-color);
69
+ --bar-height: 2px;
70
+ --bar-border-radius: 8px;
71
+ --knob-background: var(--editor-color);
72
+ --knob-size: 20px;
73
+ --pin-background: none;
74
+ --pin-color: var(--editor-color);
75
+ }
76
+ ion-buttons {
77
+ &[slot='start'],
78
+ &[slot='end'] {
79
+ ion-button {
80
+ --background: var(--editor-background-tint) !important;
81
+ --border-radius: 50%;
82
+ margin: 0 8px;
83
+ min-width: 32px;
84
+ min-height: 32px;
85
+ }
86
+ }
87
+
88
+ &.submenu-icon-buttons {
89
+ ion-button {
90
+ margin: 16px;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,21 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { PhotoEditorPage } from './photo-editor.page';
3
+ import { testConfig } from '../../../../../util/test.config';
4
+
5
+ describe('PhotoEditorPage', () => {
6
+ let component: PhotoEditorPage;
7
+ let fixture: ComponentFixture<PhotoEditorPage>;
8
+
9
+ beforeEach(() => {
10
+ TestBed.configureTestingModule({
11
+ providers: testConfig.providers,
12
+ });
13
+ fixture = TestBed.createComponent(PhotoEditorPage);
14
+ component = fixture.componentInstance;
15
+ fixture.detectChanges();
16
+ });
17
+
18
+ it('should create', () => {
19
+ expect(component).toBeTruthy();
20
+ });
21
+ });
@@ -0,0 +1,221 @@
1
+ import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ computed,
6
+ effect,
7
+ ElementRef,
8
+ inject,
9
+ input,
10
+ OnDestroy,
11
+ OnInit,
12
+ signal,
13
+ viewChild,
14
+ } from '@angular/core';
15
+ import { CommonModule } from '@angular/common';
16
+ import { FormsModule } from '@angular/forms';
17
+ import { IonContent, ModalController, RangeCustomEvent, ViewDidEnter, ViewDidLeave } from '@ionic/angular/standalone';
18
+ import ImageEditor from 'tui-image-editor';
19
+ import { filterPreset } from '../../filter-preset';
20
+ import { Subscription } from 'rxjs';
21
+ import { toObservable } from '@angular/core/rxjs-interop';
22
+ import { IDictionaryForEditor, IFilter, IPhotoEditorDismiss, ISize } from '../../types';
23
+ import { ionComponents } from '../../ion-components';
24
+ import { dictionaryForEditor } from '../../dictionaries';
25
+ import { initializeEditorIcons, waitToFindDom } from '../util';
26
+
27
+ @Component({
28
+ selector: 'app-editor-image',
29
+ templateUrl: './photo-editor.page.html',
30
+ styleUrls: ['../core.scss', './photo-editor.page.scss'],
31
+ imports: [CommonModule, FormsModule, ...ionComponents],
32
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
+ })
34
+ export class PhotoEditorPage implements OnInit, OnDestroy, ViewDidEnter, ViewDidLeave {
35
+ readonly modalCtrl = inject(ModalController);
36
+
37
+ protected readonly dictionary = signal<IDictionaryForEditor>(dictionaryForEditor());
38
+
39
+ readonly requireSquare = input<boolean, BooleanInput>(false, {
40
+ transform: coerceBooleanProperty,
41
+ });
42
+ readonly labels = input<Partial<IDictionaryForEditor> | undefined>(undefined);
43
+ readonly setLabels = effect(() => {
44
+ if (this.labels()) {
45
+ this.dictionary.update((value) => ({ ...value, ...this.labels() }));
46
+ }
47
+ });
48
+ readonly value = input.required<string>();
49
+
50
+ readonly editorRef = viewChild.required<ElementRef>('imageEditor');
51
+ readonly ionContent = viewChild.required(IonContent, { read: ElementRef });
52
+
53
+ readonly filters = signal<IFilter[]>([]);
54
+ readonly footerMenu = signal<'filter' | 'menu' | 'crop' | 'brightness'>('menu');
55
+ readonly currentCrop = signal<'cover' | '16/9' | '1' | 'auto'>('cover');
56
+ readonly currentRotate = signal<number>(0);
57
+ readonly photoCrop = signal<ISize>({
58
+ width: 0,
59
+ height: 0,
60
+ });
61
+ readonly isCropped = signal<boolean>(false);
62
+ private readonly adoptFilter = signal<IFilter | undefined>(undefined);
63
+ protected readonly filterPreset = computed(() => filterPreset(this.dictionary()));
64
+
65
+ private readonly footerMenu$ = toObservable(this.footerMenu);
66
+
67
+ private readonly initSubscription$: Subscription[] = [];
68
+ private readonly filterImageSize = 240;
69
+ private editorInstance!: ImageEditor;
70
+
71
+ private canvasContainerObserver: MutationObserver = new MutationObserver((mutationsList: MutationRecord[]) => {
72
+ if (mutationsList.find((mutation) => mutation.type === 'attributes' && mutation.attributeName === 'style')) {
73
+ // Cover the image editor with the parent element
74
+ const editorRef = this.editorRef();
75
+ editorRef.nativeElement.style.minWidth = mutationsList[0].target.parentElement!.style.maxWidth;
76
+ editorRef.nativeElement.style.minHeight = mutationsList[0].target.parentElement!.style.maxHeight;
77
+
78
+ this.photoCrop.set({
79
+ width: mutationsList[0].target.parentElement!.querySelector('canvas')!.width,
80
+ height: mutationsList[0].target.parentElement!.querySelector('canvas')!.height,
81
+ });
82
+ }
83
+ });
84
+
85
+ constructor() {
86
+ initializeEditorIcons();
87
+ }
88
+
89
+ ngOnInit() {
90
+ this.initSubscription$.push(
91
+ this.footerMenu$.subscribe((value) => {
92
+ if (value === 'filter') {
93
+ this.initializeFilterMenu().then();
94
+ }
95
+ if (value === 'crop') {
96
+ this.editorInstance.startDrawingMode('CROPPER');
97
+ this.changeCrop(this.requireSquare() ? '1' : 'cover');
98
+ }
99
+ }),
100
+ );
101
+ }
102
+
103
+ ngOnDestroy() {
104
+ this.initSubscription$.forEach((subscription) => subscription.unsubscribe());
105
+ }
106
+
107
+ async ionViewDidEnter() {
108
+ this.editorInstance = new ImageEditor(this.editorRef().nativeElement, {
109
+ cssMaxWidth: this.ionContent().nativeElement.clientWidth - 32,
110
+ cssMaxHeight: this.ionContent().nativeElement.clientHeight - 32,
111
+ });
112
+ waitToFindDom(this.editorRef().nativeElement, '.tui-image-editor-canvas-container').then(() => {
113
+ this.canvasContainerObserver.observe(this.editorRef().nativeElement.querySelector('.tui-image-editor-canvas-container'), {
114
+ attributes: true,
115
+ childList: false,
116
+ subtree: true,
117
+ });
118
+ });
119
+ const blob = await fetch(this.value()).then((res) => res.blob());
120
+ await this.editorInstance.loadImageFromFile(new File([blob], 'data.png', { type: blob.type }));
121
+ this.footerMenu.set(this.requireSquare() ? 'crop' : 'menu');
122
+ }
123
+
124
+ ionViewDidLeave() {
125
+ this.editorInstance.destroy();
126
+ this.canvasContainerObserver.disconnect();
127
+ }
128
+
129
+ changeCrop(crop: 'cover' | '16/9' | '1' | 'auto') {
130
+ const rect = crop === 'cover' ? this.photoCrop().width / this.photoCrop().height : crop === '16/9' ? 16 / 9 : 1;
131
+ this.editorInstance.setCropzoneRect(crop !== 'auto' ? rect : undefined);
132
+ this.currentCrop.set(crop);
133
+ }
134
+
135
+ async rotate() {
136
+ this.editorInstance.stopDrawingMode();
137
+ await this.editorInstance.rotate(90);
138
+ this.currentRotate.update((value) => value + 90);
139
+ this.editorInstance.startDrawingMode('CROPPER');
140
+ requestAnimationFrame(() => this.changeCrop(this.currentCrop()));
141
+ }
142
+
143
+ async closeCrop(type: 'cancel' | 'apply') {
144
+ if (this.footerMenu() === 'crop') {
145
+ if (type === 'cancel') {
146
+ await this.editorInstance.rotate(this.currentRotate() * -1);
147
+ } else {
148
+ await this.editorInstance.crop(this.editorInstance.getCropzoneRect());
149
+ this.isCropped.set(true);
150
+ }
151
+ this.currentRotate.set(0);
152
+ this.currentCrop.set('cover');
153
+ this.editorInstance.stopDrawingMode();
154
+ } else if (this.footerMenu() === 'brightness') {
155
+ if (type === 'cancel' && this.editorInstance.hasFilter('brightness')) {
156
+ await this.editorInstance.removeFilter('brightness');
157
+ }
158
+ }
159
+ this.footerMenu.set('menu');
160
+ }
161
+
162
+ async changeRange(event: RangeCustomEvent) {
163
+ if (this.editorInstance.hasFilter('brightness')) {
164
+ await this.editorInstance.removeFilter('brightness');
165
+ }
166
+ await this.editorInstance.applyFilter('brightness', {
167
+ brightness: Number(event.detail.value) / 255,
168
+ });
169
+ }
170
+
171
+ imageSave() {
172
+ const value = this.editorInstance.toDataURL();
173
+ this.modalCtrl.dismiss({ value } as IPhotoEditorDismiss);
174
+ }
175
+
176
+ private async initializeFilterMenu() {
177
+ const filters: IFilter[] = [];
178
+
179
+ const defaultInstance = new ImageEditor(document.createElement('div'), {
180
+ cssMaxWidth: this.filterImageSize,
181
+ cssMaxHeight: (this.photoCrop().height * this.filterImageSize) / this.photoCrop().width,
182
+ });
183
+ const blob = await fetch(
184
+ this.editorInstance.toDataURL({
185
+ multiplier: this.filterImageSize / this.photoCrop().width,
186
+ }),
187
+ ).then((res) => res.blob());
188
+ await defaultInstance.loadImageFromFile(new File([blob], 'defaultInstance.png', { type: blob.type }));
189
+
190
+ for (const filter of this.filterPreset()) {
191
+ if (filter.type !== 'Default') {
192
+ await defaultInstance.applyFilter(filter.type, filter.option);
193
+ }
194
+ filters.push({
195
+ name: filter.name,
196
+ type: filter.type,
197
+ option: filter.option,
198
+ data: defaultInstance.toDataURL(),
199
+ width: this.filterImageSize,
200
+ height: (this.photoCrop().height * this.filterImageSize) / this.photoCrop().width,
201
+ });
202
+ if (filter.type !== 'Default') {
203
+ await defaultInstance.removeFilter(filter.type);
204
+ }
205
+ }
206
+ this.filters.set(filters);
207
+ defaultInstance.destroy();
208
+ }
209
+
210
+ async filterImage(filter: IFilter) {
211
+ if (this.adoptFilter()) {
212
+ await this.editorInstance.removeFilter(this.adoptFilter()!.type);
213
+ }
214
+ if (filter.type === 'Default') {
215
+ this.adoptFilter.set(undefined);
216
+ return;
217
+ }
218
+ await this.editorInstance.applyFilter(filter.type, filter.option);
219
+ this.adoptFilter.set(filter);
220
+ }
221
+ }
@@ -0,0 +1,31 @@
1
+ <ion-header [translucent]="true">
2
+ <ion-toolbar>
3
+ <ion-buttons slot="start">
4
+ <ion-button (click)="modalCtrl.dismiss()" class="viewer-close-button">
5
+ <ion-icon name="close-outline" slot="icon-only"></ion-icon>
6
+ </ion-button>
7
+ </ion-buttons>
8
+ @if (enableDelete()) {
9
+ <ion-buttons slot="end">
10
+ <ion-button color="photo-editor-danger" fill="solid" type="submit" (click)="remove()">{{ dictionary().delete }}</ion-button>
11
+ </ion-buttons>
12
+ }
13
+ </ion-toolbar>
14
+ </ion-header>
15
+
16
+ <ion-content [fullscreen]="true">
17
+ <swiper-container #swiper>
18
+ @for (url of imageUrls(); track url) {
19
+ <swiper-slide>
20
+ <div class="swiper-zoom-container"><img [src]="url" alt="" [class.circle]="isCircle()" /></div>
21
+ </swiper-slide>
22
+ }
23
+ </swiper-container>
24
+ </ion-content>
25
+
26
+ @if (enableFooterSafeArea()) {
27
+ <!-- use for safe area-->
28
+ <ion-footer>
29
+ <ion-toolbar style="--border-width: 0; --min-height: 0"></ion-toolbar>
30
+ </ion-footer>
31
+ }