@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.
- package/eslint.config.js +7 -0
- package/ng-package.json +7 -0
- package/package.json +6 -17
- package/src/lib/dictionaries.ts +31 -0
- package/src/lib/filter-preset.ts +44 -0
- package/src/lib/ion-components.ts +3 -0
- package/src/lib/pages/core.scss +63 -0
- package/src/lib/pages/photo-editor/photo-editor.page.html +95 -0
- package/src/lib/pages/photo-editor/photo-editor.page.scss +95 -0
- package/src/lib/pages/photo-editor/photo-editor.page.spec.ts +21 -0
- package/src/lib/pages/photo-editor/photo-editor.page.ts +221 -0
- package/src/lib/pages/photo-viewer/photo-viewer.page.html +31 -0
- package/src/lib/pages/photo-viewer/photo-viewer.page.scss +29 -0
- package/src/lib/pages/photo-viewer/photo-viewer.page.spec.ts +25 -0
- package/src/lib/pages/photo-viewer/photo-viewer.page.ts +134 -0
- package/src/lib/pages/util.ts +48 -0
- package/src/lib/photoEditorErrors.ts +5 -0
- package/src/lib/services/photo-file.service.spec.ts +19 -0
- package/src/lib/services/photo-file.service.ts +161 -0
- package/src/lib/types.ts +55 -0
- package/src/public-api.ts +8 -0
- package/tsconfig.lib.json +14 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +14 -0
- package/fesm2022/rdlabo-ionic-angular-photo-editor.mjs +0 -533
- package/fesm2022/rdlabo-ionic-angular-photo-editor.mjs.map +0 -1
- package/index.d.ts +0 -132
package/eslint.config.js
ADDED
package/ng-package.json
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rdlabo/ionic-angular-photo-editor",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "21.0.1",
|
|
4
4
|
"peerDependencies": {
|
|
5
|
-
"@angular/cdk": "^
|
|
6
|
-
"@angular/common": "^
|
|
7
|
-
"@angular/core": "^
|
|
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
|
-
|
|
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,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
|
+
}
|