Add fileName property to models and update related components for file handling

This commit is contained in:
liosha84
2025-08-09 22:22:02 +03:00
parent ca73cf31df
commit d45fd83593
64 changed files with 1644 additions and 433 deletions
+1 -7
View File
@@ -9,7 +9,6 @@ import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import {AppModule} from './app.module';
import {providePrimeNG} from 'primeng/config';
import Aura from '@primeng/themes/aura';
import {provideAnimations} from '@angular/platform-browser/animations';
import {provideMarkdown} from 'ngx-markdown';
@@ -24,11 +23,6 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes), provideClientHydration(withEventReplay()),
provideMarkdown(),
importProvidersFrom(AppModule),
providePrimeNG({
theme: {
preset: Aura
}
})
importProvidersFrom(AppModule)
]
};
+25 -2
View File
@@ -1,6 +1,10 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {Router, RouterOutlet} from '@angular/router';
import {SpinnerComponent} from './components/spinner/spinner.component';
import {EventBusService} from './_shared/event-bus.service';
import {AuthService} from './services/auth.service';
import {TokenStorageService} from './services/token-storage.service';
import {GlobalConstants} from './global-constants';
@Component({
selector: 'app-root',
@@ -15,7 +19,26 @@ import {SpinnerComponent} from './components/spinner/spinner.component';
export class App {
protected title = 'jambotron-ui';
constructor() {
constructor(
private eventBusService:EventBusService,
private authService:AuthService,
private tokenStorageService:TokenStorageService,
private router: Router
) {
this.eventBusService.on('logout', () => {
console.log('eventBusService logout');
this.authService.logout().subscribe({
next: res => {
console.log(res);
this.tokenStorageService.clean();
router.navigate([GlobalConstants.DEFAULT_PAGE]).then();
},
error: err => {
console.log(err);
}
});
})
}
}
@@ -14,7 +14,7 @@
</mat-toolbar>
<input
type="file"
id="fileInput"
fileName="fileInput"
(change)="selectFile($event)"
name="fileInput"
/>
@@ -0,0 +1,103 @@
<!--
<div class="ngx-pe-overlay-container">
<div class="ngx-pe-dialog-container" [style.max-width]="modalMaxWidth">
<div class="ngx-pe-dialog-header" *ngIf="!hideModalHeader">
{{modalTitle}}
</div>
<div class="ngx-pe-dialog-body">
<div class="ngx-pe-cropper-wrapper">
<div class="ngx-pe-loading" *ngIf="!imageLoaded" [class.ngx-pe-processing]="isProcessing">
<svg xmlns="http://www.w3.org/2000/svg"
style="margin: auto; display: block; shape-rendering: auto; animation-play-state: running; animation-delay: 0s;"
viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" fill="none" stroke="#f3f3f3" stroke-width="10" r="35"
stroke-dasharray="164.93361431346415 56.97787143782138"
style="animation-play-state: running; animation-delay: 0s;">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s"
values="0 50 50;360 50 50" keyTimes="0;1"
style="animation-play-state: running; animation-delay: 0s;"></animateTransform>
</circle>
</svg>
</div>
<img class="ngx-pe-img" #image loading="lazy" (load)="onLoad()" (error)="error()" [src]="imgUrl" alt="cropper-img" crossorigin="anonymous">
<div class="ngx-pe-tool-bar">
&lt;!&ndash;move&ndash;&gt;
<button (click)="move()" title="Move">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M24 44 15.85 35.85 18.05 33.65 22.5 38.1V28.65H25.5V38.1L29.95 33.65L32.15 35.85ZM11.9 31.9 4 24 11.95 16.05 14.15 18.25 9.9 22.5H19.35V25.5H9.9L14.1 29.7ZM36.1 31.9 33.9 29.7 38.1 25.5H28.7V22.5H38.1L33.9 18.3L36.1 16.1L44 24ZM22.5 19.3V9.9L18.3 14.1L16.1 11.9L24 4L31.9 11.9L29.7 14.1L25.5 9.9V19.3Z"/>
</svg>
</button>
&lt;!&ndash;crop&ndash;&gt;
<button (click)="crop()" title="Crop">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M34.75 31.75V13.25Q34.75 13.25 34.75 13.25Q34.75 13.25 34.75 13.25H16.25V10.25H34.75Q35.95 10.25 36.85 11.15Q37.75 12.05 37.75 13.25V31.75ZM36.25 46Q35.6 46 35.175 45.575Q34.75 45.15 34.75 44.5V37.75H13.25Q12.05 37.75 11.15 36.85Q10.25 35.95 10.25 34.75V13.25H3.5Q2.85 13.25 2.425 12.825Q2 12.4 2 11.75Q2 11.1 2.425 10.675Q2.85 10.25 3.5 10.25H10.25V3.5Q10.25 2.85 10.675 2.425Q11.1 2 11.75 2Q12.4 2 12.825 2.425Q13.25 2.85 13.25 3.5V34.75Q13.25 34.75 13.25 34.75Q13.25 34.75 13.25 34.75H44.5Q45.15 34.75 45.575 35.175Q46 35.6 46 36.25Q46 36.9 45.575 37.325Q45.15 37.75 44.5 37.75H37.75V44.5Q37.75 45.15 37.325 45.575Q36.9 46 36.25 46Z"/>
</svg>
</button>
&lt;!&ndash;zoom in&ndash;&gt;
<button (click)="zoomIn()" title="Zoom In">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M38.7 40.85 26.65 28.8Q25.15 30.1 23.15 30.825Q21.15 31.55 18.9 31.55Q13.5 31.55 9.75 27.8Q6 24.05 6 18.75Q6 13.45 9.75 9.7Q13.5 5.95 18.85 5.95Q24.15 5.95 27.875 9.7Q31.6 13.45 31.6 18.75Q31.6 20.9 30.9 22.9Q30.2 24.9 28.8 26.65L40.95 38.7Q41.4 39.1 41.375 39.75Q41.35 40.4 40.9 40.85Q40.45 41.3 39.8 41.3Q39.15 41.3 38.7 40.85ZM18.85 28.55Q22.9 28.55 25.75 25.675Q28.6 22.8 28.6 18.75Q28.6 14.7 25.75 11.825Q22.9 8.95 18.85 8.95Q14.75 8.95 11.875 11.825Q9 14.7 9 18.75Q9 22.8 11.875 25.675Q14.75 28.55 18.85 28.55ZM18.8 24.3Q18.15 24.3 17.725 23.875Q17.3 23.45 17.3 22.8V20.2H14.65Q14.05 20.2 13.625 19.75Q13.2 19.3 13.2 18.65Q13.2 18.05 13.625 17.625Q14.05 17.2 14.7 17.2H17.3V14.6Q17.3 14 17.75 13.575Q18.2 13.15 18.85 13.15Q19.45 13.15 19.875 13.575Q20.3 14 20.3 14.65V17.2H22.9Q23.5 17.2 23.925 17.65Q24.35 18.1 24.35 18.7Q24.35 19.35 23.925 19.775Q23.5 20.2 22.85 20.2H20.3V22.85Q20.3 23.45 19.85 23.875Q19.4 24.3 18.8 24.3Z"/>
</svg>
</button>
&lt;!&ndash;zoom out&ndash;&gt;
<button (click)="zoomOut()" title="Zoom Out">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M38.7 40.85 26.65 28.8Q25.15 30.1 23.15 30.825Q21.15 31.55 18.9 31.55Q13.5 31.55 9.75 27.8Q6 24.05 6 18.75Q6 13.45 9.75 9.7Q13.5 5.95 18.85 5.95Q24.15 5.95 27.875 9.7Q31.6 13.45 31.6 18.75Q31.6 20.9 30.9 22.9Q30.2 24.9 28.8 26.65L40.95 38.7Q41.4 39.1 41.375 39.75Q41.35 40.4 40.9 40.85Q40.45 41.3 39.8 41.3Q39.15 41.3 38.7 40.85ZM18.85 28.55Q22.9 28.55 25.75 25.675Q28.6 22.8 28.6 18.75Q28.6 14.7 25.75 11.825Q22.9 8.95 18.85 8.95Q14.75 8.95 11.875 11.825Q9 14.7 9 18.75Q9 22.8 11.875 25.675Q14.75 28.55 18.85 28.55ZM15.2 20.2Q14.6 20.2 14.175 19.75Q13.75 19.3 13.75 18.65Q13.75 18.05 14.175 17.625Q14.6 17.2 15.25 17.2H22.35Q22.95 17.2 23.375 17.65Q23.8 18.1 23.8 18.7Q23.8 19.35 23.375 19.775Q22.95 20.2 22.3 20.2Z"/>
</svg>
</button>
&lt;!&ndash;rotate left&ndash;&gt;
<button (click)="rotateLeft()" title="Rotate Left">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M7.9 23.6Q7.15 23.6 6.7 23.075Q6.25 22.55 6.4 21.85Q6.75 20.3 7.325 18.85Q7.9 17.4 8.7 16.05Q9.1 15.4 9.775 15.35Q10.45 15.3 11 15.85Q11.4 16.2 11.45 16.75Q11.5 17.3 11.25 17.75Q10.55 18.9 10.075 20.075Q9.6 21.25 9.35 22.4Q9.25 22.95 8.85 23.275Q8.45 23.6 7.9 23.6ZM20.05 43.7Q18.6 43.35 17.15 42.775Q15.7 42.2 14.4 41.4Q13.75 41 13.7 40.3Q13.65 39.6 14.2 39Q14.55 38.65 15.05 38.6Q15.55 38.55 15.95 38.8Q17.1 39.5 18.225 39.975Q19.35 40.45 20.55 40.75Q21.05 40.85 21.4 41.275Q21.75 41.7 21.75 42.25Q21.75 43 21.25 43.425Q20.75 43.85 20.05 43.7ZM8.7 35.7Q7.9 34.4 7.325 32.925Q6.75 31.45 6.4 29.85Q6.25 29.15 6.675 28.625Q7.1 28.1 7.9 28.1Q8.4 28.1 8.825 28.425Q9.25 28.75 9.35 29.3Q9.6 30.55 10.05 31.725Q10.5 32.9 11.2 33.95Q11.5 34.4 11.45 34.95Q11.4 35.5 11 35.9Q10.45 36.45 9.775 36.4Q9.1 36.35 8.7 35.7ZM27.95 43.65Q27.25 43.85 26.75 43.425Q26.25 43 26.25 42.25Q26.25 41.75 26.6 41.325Q26.95 40.9 27.45 40.75Q32.5 39.4 35.75 35.375Q39 31.35 39 25.85Q39 19.5 34.675 15.175Q30.35 10.85 24 10.85H23L25.85 13.7Q26.3 14.15 26.3 14.8Q26.3 15.45 25.85 15.9Q25.4 16.35 24.75 16.35Q24.1 16.35 23.65 15.9L18.15 10.4Q17.9 10.15 17.8 9.9Q17.7 9.65 17.7 9.35Q17.7 9.05 17.8 8.8Q17.9 8.55 18.15 8.3L23.65 2.8Q24.1 2.35 24.75 2.35Q25.4 2.35 25.85 2.8Q26.3 3.25 26.3 3.9Q26.3 4.55 25.85 5L23 7.85H24Q31.55 7.85 36.775 13.075Q42 18.3 42 25.85Q42 32.4 38.075 37.275Q34.15 42.15 27.95 43.65Z"/>
</svg>
</button>
&lt;!&ndash;rotate right&ndash;&gt;
<button (click)="rotateRight()" title="Rotate Right">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M40.2 23.6Q39.7 23.6 39.275 23.25Q38.85 22.9 38.75 22.4Q38.5 21.2 38.025 20.05Q37.55 18.9 36.85 17.75Q36.6 17.3 36.65 16.75Q36.7 16.2 37.1 15.8Q37.65 15.25 38.35 15.325Q39.05 15.4 39.45 16.05Q40.25 17.35 40.8 18.775Q41.35 20.2 41.7 21.8Q41.85 22.5 41.4 23.05Q40.95 23.6 40.2 23.6ZM26.3 42.2Q26.3 41.7 26.65 41.275Q27 40.85 27.5 40.75Q28.65 40.5 29.8 40.025Q30.95 39.55 32.1 38.85Q32.55 38.6 33.075 38.625Q33.6 38.65 34 39.05Q34.55 39.6 34.475 40.3Q34.4 41 33.75 41.4Q32.4 42.25 30.975 42.8Q29.55 43.35 28.1 43.7Q27.4 43.85 26.85 43.4Q26.3 42.95 26.3 42.2ZM37.1 35.9Q36.75 35.55 36.7 34.975Q36.65 34.4 36.9 33.95Q37.6 32.8 38.05 31.65Q38.5 30.5 38.75 29.3Q38.85 28.8 39.25 28.45Q39.65 28.1 40.2 28.1Q40.95 28.1 41.375 28.625Q41.8 29.15 41.65 29.85Q41.25 31.45 40.7 32.9Q40.15 34.35 39.4 35.65Q39 36.3 38.325 36.375Q37.65 36.45 37.1 35.9ZM20.05 43.7Q13.9 42.3 10 37.35Q6.1 32.4 6.1 25.85Q6.1 18.3 11.325 13.075Q16.55 7.85 24.1 7.85H25.1L22.25 5Q21.8 4.55 21.8 3.9Q21.8 3.25 22.25 2.8Q22.7 2.35 23.35 2.35Q24 2.35 24.45 2.8L29.95 8.3Q30.2 8.55 30.3 8.8Q30.4 9.05 30.4 9.35Q30.4 9.65 30.3 9.9Q30.2 10.15 29.95 10.4L24.45 15.9Q24 16.35 23.35 16.35Q22.7 16.35 22.25 15.9Q21.8 15.45 21.8 14.8Q21.8 14.15 22.25 13.7L25.1 10.85H24.1Q17.75 10.85 13.425 15.175Q9.1 19.5 9.1 25.85Q9.1 31.35 12.35 35.375Q15.6 39.4 20.65 40.7Q21.15 40.8 21.5 41.225Q21.85 41.65 21.85 42.2Q21.85 42.95 21.3 43.4Q20.75 43.85 20.05 43.7Z"/>
</svg>
</button>
&lt;!&ndash;flip vertical&ndash;&gt;
<button (click)="flipH()" title="Flip Horizontal">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M9 42Q7.8 42 6.9 41.1Q6 40.2 6 39V9Q6 7.8 6.9 6.9Q7.8 6 9 6H18.1Q18.75 6 19.175 6.425Q19.6 6.85 19.6 7.5Q19.6 8.15 19.175 8.575Q18.75 9 18.1 9H9Q9 9 9 9Q9 9 9 9V39Q9 39 9 39Q9 39 9 39H18.1Q18.75 39 19.175 39.425Q19.6 39.85 19.6 40.5Q19.6 41.15 19.175 41.575Q18.75 42 18.1 42ZM24.1 46Q23.45 46 23.025 45.575Q22.6 45.15 22.6 44.5V3.5Q22.6 2.85 23.025 2.425Q23.45 2 24.1 2Q24.75 2 25.175 2.425Q25.6 2.85 25.6 3.5V44.5Q25.6 45.15 25.175 45.575Q24.75 46 24.1 46ZM39 9H38.3V6H39Q40.2 6 41.1 6.9Q42 7.8 42 9V9.7H39ZM39 26.75V21.25H42V26.75ZM39 42H38.3V39H39V38.3H42V39Q42 40.2 41.1 41.1Q40.2 42 39 42ZM39 18.25V12.7H42V18.25ZM39 35.3V29.75H42V35.3ZM28.6 42V39H35.3V42ZM28.6 9V6H35.3V9Z"/>
</svg>
</button>
&lt;!&ndash;flip horizontal&ndash;&gt;
<button (click)="flipV()" title="Flip Vertical">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" transform='rotate(90)'>
<path
d="M9 42Q7.8 42 6.9 41.1Q6 40.2 6 39V9Q6 7.8 6.9 6.9Q7.8 6 9 6H18.1Q18.75 6 19.175 6.425Q19.6 6.85 19.6 7.5Q19.6 8.15 19.175 8.575Q18.75 9 18.1 9H9Q9 9 9 9Q9 9 9 9V39Q9 39 9 39Q9 39 9 39H18.1Q18.75 39 19.175 39.425Q19.6 39.85 19.6 40.5Q19.6 41.15 19.175 41.575Q18.75 42 18.1 42ZM24.1 46Q23.45 46 23.025 45.575Q22.6 45.15 22.6 44.5V3.5Q22.6 2.85 23.025 2.425Q23.45 2 24.1 2Q24.75 2 25.175 2.425Q25.6 2.85 25.6 3.5V44.5Q25.6 45.15 25.175 45.575Q24.75 46 24.1 46ZM39 9H38.3V6H39Q40.2 6 41.1 6.9Q42 7.8 42 9V9.7H39ZM39 26.75V21.25H42V26.75ZM39 42H38.3V39H39V38.3H42V39Q42 40.2 41.1 41.1Q40.2 42 39 42ZM39 18.25V12.7H42V18.25ZM39 35.3V29.75H42V35.3ZM28.6 42V39H35.3V42ZM28.6 9V6H35.3V9Z"/>
</svg>
</button>
<button (click)="reset()" title="Reset">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M9.8 31.45Q8.8 29.65 8.4 27.825Q8 26 8 24.1Q8 17.55 12.725 12.825Q17.45 8.1 24 8.1H26.15L22.15 4.1L24.1 2.15L31.55 9.6L24.1 17.05L22.1 15.05L26.05 11.1H24Q18.65 11.1 14.825 14.925Q11 18.75 11 24.1Q11 25.55 11.275 26.85Q11.55 28.15 11.95 29.3ZM23.8 46 16.35 38.55 23.8 31.1 25.75 33.05 21.75 37.05H24Q29.35 37.05 33.175 33.225Q37 29.4 37 24.05Q37 22.6 36.75 21.3Q36.5 20 36 18.85L38.15 16.7Q39.15 18.5 39.575 20.325Q40 22.15 40 24.05Q40 30.6 35.275 35.325Q30.55 40.05 24 40.05H21.75L25.75 44.05Z"/>
</svg>
</button>
</div>
</div>
</div>
<div class="ngx-pe-dialog-footer">
<button class="ngx-pe-btn ngx-pe-btn-outline-light" (click)="close()">{{closeBtnText}}</button>
<button class="ngx-pe-btn ngx-pe-btn-primary" (click)="export()">{{applyBtnText}}</button>
</div>
</div>
</div>
-->
@@ -0,0 +1,179 @@
@import "~node_modules/cropperjs/dist/cropper.css";
.ngx-pe-overlay-container {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.25);
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 99999;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
padding: 1rem;
box-sizing: border-box;
}
.ngx-pe-overlay-container * {
box-sizing: border-box;
}
.ngx-pe-dialog-container {
position: relative;
border-radius: 4px;
overflow: hidden;
outline: 0;
width: 100%;
height: auto;
max-height: 95vh;
box-shadow: 0 11px 15px -7px #0003, 0 24px 38px 3px #00000024, 0 9px 46px 8px #0000001f;
background: #333333;
color: white;
display: flex;
flex-direction: column;
}
.ngx-pe-dialog-header {
padding: 1rem 1rem 1rem 1rem;
font-size: 1rem;
font-weight: 400;
}
.ngx-pe-dialog-body {
height: 100%;
position: relative;
box-sizing: border-box;
}
.ngx-pe-dialog-footer {
padding: 1rem 1rem;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.ngx-pe-cropper-wrapper {
position: relative;
}
.ngx-pe-img {
display: block;
max-width: 100%;
height: auto;
max-height: 300px;
}
.ngx-pe-tool-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
background-color: #222;
box-sizing: border-box;
width: 100%;
}
.ngx-pe-tool-bar button {
background-color: transparent;
border-width: 0;
color: #fff;
cursor: pointer;
float: left;
font-size: 0.875rem;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
width: 2.5rem;
}
.ngx-pe-tool-bar button:hover {
background-color: #0074d9;
color: #fff;
}
.ngx-pe-tool-bar svg {
width: 20px;
height: 20px;
fill: white;
}
.ngx-pe-btn {
--bs-btn-padding-y: 0.25rem;
--bs-btn-padding-x: 1rem;
--bs-btn-font-size: 0.875rem;
--bs-btn-border-radius: 0.25rem;
--bs-btn-font-weight: 400;
--bs-btn-line-height: 1.5;
--bs-btn-color: #212529;
--bs-btn-bg: transparent;
--bs-btn-border-width: 1px;
--bs-btn-border-color: transparent;
--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
--bs-btn-disabled-opacity: 0.65;
--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb));
display: inline-block;
padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
font-size: var(--bs-btn-font-size);
font-weight: var(--bs-btn-font-weight);
line-height: var(--bs-btn-line-height);
color: var(--bs-btn-color);
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
border-radius: var(--bs-btn-border-radius);
background-color: var(--bs-btn-bg);
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.ngx-pe-btn:hover {
color: var(--bs-btn-hover-color);
background-color: var(--bs-btn-hover-bg);
border-color: var(--bs-btn-hover-border-color);
}
.ngx-pe-btn-outline-light {
--bs-btn-color: #f8f9fa;
--bs-btn-border-color: #f8f9fa;
--bs-btn-hover-color: #000;
--bs-btn-hover-bg: #f8f9fa;
--bs-btn-hover-border-color: #f8f9fa;
--bs-btn-focus-shadow-rgb: 248, 249, 250;
--bs-btn-active-color: #000;
--bs-btn-active-bg: #f8f9fa;
--bs-btn-active-border-color: #f8f9fa;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #f8f9fa;
--bs-btn-disabled-bg: transparent;
--bs-gradient: none;
}
.ngx-pe-btn-primary {
--bs-btn-color: #fff;
--bs-btn-bg: #0d6efd;
--bs-btn-border-color: #0d6efd;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #0b5ed7;
--bs-btn-hover-border-color: #0a58ca;
--bs-btn-focus-shadow-rgb: 49, 132, 253;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #0a58ca;
--bs-btn-active-border-color: #0a53be;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #0d6efd;
--bs-btn-disabled-border-color: #0d6efd;
}
.ngx-pe-loading {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 2;
background-color: #222222;
display: flex;
justify-content: center;
align-items: center;
transition: all 10s ease;
}
.ngx-pe-processing {
background-color: rgba(34,34,34, 0.8);
}
.ngx-pe-loading svg {
width: 50px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PhotoEditComponent } from './photo-edit.component';
describe('PhotoEditComponent', () => {
let component: PhotoEditComponent;
let fixture: ComponentFixture<PhotoEditComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PhotoEditComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PhotoEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,228 @@
/*
import {Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild} from '@angular/core';
import Cropper from "cropperjs";
import ViewMode = Cropper.ViewMode;
import {NgIf, NgOptimizedImage} from '@angular/common';
@Component({
selector: 'app-photo-edit.component',
imports: [
NgIf,
NgOptimizedImage
],
templateUrl: './photo-edit.component.html',
styleUrl: './photo-edit.component.scss'
})
export class PhotoEditComponent implements OnDestroy {
@ViewChild('image')
image!: ElementRef;
private cropper!: Cropper;
public imgUrl!: string;
private FILES_REGEX = /\.(gif|jpe?g|tiff|png|webp|bmp)$/i;
@Input() modalTitle:String = 'Photo Editor';
@Input() hideModalHeader = false;
@Input() aspectRatio = NaN;
@Input() autoCropArea = 0.8;
@Input() autoCrop = true;
@Input() mask = true;
@Input() guides = true;
@Input() centerIndicator = true;
@Input() viewMode:ViewMode = 0;
@Input() modalMaxWidth = '500px';
@Input() modalCentered = false;
@Input() scalable = true;
@Input() zoomable = true;
@Input() cropBoxMovable = true;
@Input() cropBoxResizable = true;
@Input() darkTheme = true;
@Input() roundCropper = false;
@Input() canvasHeight = 400;
@Input() resizeToWidth: number | any;
@Input() resizeToHeight: number | any;
@Input() imageSmoothingEnabled = true;
@Input() imageSmoothingQuality: ImageSmoothingQuality = 'high';
@Input() format: string | any;
@Input() closeBtnText = 'Close';
@Input() applyBtnText = 'Apply';
quality = 92;
@Input() set imageQuality(value: number) {
if (value > 0 && value <= 100) {
this.quality = value;
} else {
this.errorEvent.error('Invalid image quality');
}
}
@Output() closeEvent = new EventEmitter<any>();
@Output() imageCroppedEvent = new EventEmitter<PhotoEditCroppedEvent>();
@Output() errorEvent = new EventEmitter<any>();
imageLoaded = false;
isProcessing = false;
@Input() set source(data: File | string | any) {
if (data instanceof Event) {
// @ts-ignore
const file = (<HTMLInputElement>data.target).files[0];
if (this.FILES_REGEX.test(file.name)) {
if (!this.format) {
// @ts-ignore
this.format = data.target.files[0].type.split('/')[1];
}
const reader = new FileReader();
reader.onload = (ev: any) => {
this.imgUrl = ev.target.result;
};
// @ts-ignore
reader.readAsDataURL(data.target.files[0]);
} else {
console.log('error');
this.errorEvent.emit('Not supported INPUT');
}
} else if (data instanceof File) {
if (this.FILES_REGEX.test(data.name)) {
if (!this.format) {
this.format = data.type.split('/')[1];
}
const reader = new FileReader();
reader.onload = (ev: any) => {
this.imgUrl = ev.target.result;
};
reader.readAsDataURL(data);
} else {
this.errorEvent.emit('Not supported INPUT');
}
} else if (typeof data === 'string') {
if ((/^data:image\/([a-zA-Z]*);base64,([^\"]*)$/).test(data)) {
this.imgUrl = data;
if (!this.format) {
this.format = ((data.split(',')[0]).split(';')[0]).split(':')[1].split('/')[1];
}
} else if (this.isValidImageURL(data)) {
this.imgUrl = data;
if (!this.format) {
// @ts-ignore
this.format = data.split(/[#?]/)[0].split('.').pop().trim();
}
} else {
this.errorEvent.emit('Not supported URL');
}
} else {
this.errorEvent.emit('Not supported INPUT');
}
}
isValidImageURL(str: string) {
return str.match(/^http[^\?]*.(jpg|jpeg|gif|png|tiff|bmp)(\?(.*))?(\#(.*))?$/gim);
}
rotateRight() {
this.cropper.rotate(45);
}
rotateLeft() {
this.cropper.rotate(-45);
}
crop() {
this.cropper.setDragMode('crop');
}
move() {
this.cropper.setDragMode('move');
}
zoomIn() {
this.cropper.zoom(0.1);
}
zoomOut() {
this.cropper.zoom(-0.1);
}
flipH() {
this.cropper.scaleX(-this.cropper.getImageData().scaleX);
}
flipV() {
this.cropper.scaleY(-this.cropper.getImageData().scaleY);
}
reset() {
this.cropper.reset();
}
export() {
this.isProcessing = true;
this.imageLoaded = false;
let cropedImage = this.cropper.getCroppedCanvas({
width: this.resizeToWidth,
height: this.resizeToHeight,
imageSmoothingEnabled: this.imageSmoothingEnabled,
imageSmoothingQuality: this.imageSmoothingQuality
});
const outputImage = cropedImage.toDataURL('image/' + this.format, this.quality);
cropedImage.toBlob((blob: any) => {
this.imageCroppedEvent.emit({
base64: outputImage,
file: new File([blob],
Date.now() + '.' + this.format,
{type: 'image/' + this.format})
});
this.isProcessing = false;
this.imageLoaded = true;
}, 'image/' + this.format, this.quality / 100);
}
close() {
this.closeEvent.emit();
}
onLoad() {
this.image.nativeElement.addEventListener('ready', () => {
this.imageLoaded = true;
});
this.cropper = new Cropper(this.image.nativeElement, {
aspectRatio: this.aspectRatio,
autoCropArea: this.autoCropArea,
autoCrop: this.autoCrop,
modal: this.mask, // black mask
guides: this.guides, // grid
center: this.centerIndicator, // center indicator
viewMode: this.viewMode,
scalable: this.scalable,
zoomable: this.zoomable,
background: false,
cropBoxMovable: this.cropBoxMovable,
cropBoxResizable: this.cropBoxResizable,
});
}
ngOnDestroy(): void {
console.log('destroyed');
}
error() {
this.errorEvent.emit('Error loading image');
}
}
export interface PhotoEditCroppedEvent {
base64?: string;
file?: File;
}
export type imageFormat = 'gif' | 'jpeg' | 'tiff' | 'png' | 'webp' | 'bmp';
*/
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { PhotoEditorService } from './photo-editor.service';
describe('PhotoEditorService', () => {
let service: PhotoEditorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(PhotoEditorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,92 @@
/*
import {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
EmbeddedViewRef,
Injectable,
Injector, ViewContainerRef
} from '@angular/core';
import {Observable, Subject} from 'rxjs';
import {PhotoEditComponent, PhotoEditCroppedEvent} from './photo-edit.component';
import ViewMode = Cropper.ViewMode;
@Injectable({
providedIn: 'root'
})
export class PhotoEditorService {
private photoEditorSubscriber!: Subject<any>;
private photoEditorComponentRef!: ComponentRef<PhotoEditComponent>;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private appRef: ApplicationRef,
private injector: Injector) {
}
open(source: Event | string | File | any, options?: Options): Observable<PhotoEditCroppedEvent> {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(PhotoEditComponent);
const componentRef = componentFactory.create(this.injector);
this.appRef.attachView(componentRef.hostView);
const domElem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
document.body.appendChild(domElem);
this.photoEditorSubscriber = new Subject<string>();
this.photoEditorComponentRef = componentRef;
this.photoEditorComponentRef.instance.closeEvent.subscribe(() => this.close());
this.photoEditorComponentRef.instance.errorEvent.subscribe((data) => this.errorHandler(data));
this.photoEditorComponentRef.instance.imageCroppedEvent.subscribe((data) => this.export(data));
if (options) {
Object.keys(options).map(value => {
// @ts-ignore
this.photoEditorComponentRef.instance[value] = options[value];
})
}
this.photoEditorComponentRef.instance.source = source;
return this.photoEditorSubscriber.asObservable();
}
private errorHandler(data: any) {
this.photoEditorSubscriber.error(data);
this.close();
}
private close() {
this.photoEditorSubscriber.complete();
this.photoEditorComponentRef.destroy();
}
private export(data: any) {
this.photoEditorSubscriber.next(data);
this.close();
}
}
export interface Options {
aspectRatio?: number | any;
modalTitle?: string;
hideModalHeader?: boolean;
autoCropArea?: number;
autoCrop?: boolean;
mask?: boolean;
guides?: boolean;
centerIndicator?: boolean;
viewMode?: ViewMode;
modalMaxWidth?: string;
scalable?: boolean;
zoomable?: boolean;
cropBoxMovable?: boolean;
cropBoxResizable?: boolean;
roundCropper?: boolean;
resizeToWidth?: number | any;
resizeToHeight?: number | any;
imageSmoothingEnabled?: boolean;
imageSmoothingQuality?: ImageSmoothingQuality;
format?: string | any;
imageQuality?: number;
applyBtnText?: string;
closeBtnText?: string;
}
*/
@@ -1,5 +1,5 @@
@if (isSpinnerVisible) {
<div id="http-loader">
<div fileName="http-loader">
<div class="loader-bg">
@if (spinner() === Spinkit.skLine) {
<div class="sk-line-material" [class.colored]="!backgroundColor()">
@@ -24,9 +24,9 @@ export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// req = req.clone({
// withCredentials: true,
// });
req = req.clone({
withCredentials: true,
});
return next.handle(req).pipe(
catchError((error) => {
@@ -59,6 +59,7 @@ export class AuthInterceptor implements HttpInterceptor {
if (error.status == '403') {
this.eventBusService.emit(new EventData('logout', null));
console.log('eventBusService logout');
}
return throwError(() => error);
+2
View File
@@ -1,4 +1,6 @@
export class FileInfo {
name: string | undefined;
url: string | undefined;
mode: string | undefined;//View or CropView
base64string: string | undefined;
}
+1 -1
View File
@@ -1,4 +1,4 @@
export class Role {
id?:any;
fileName?:any;
name?:string;
}
+1 -1
View File
@@ -1,5 +1,5 @@
export class Setting {
id?:any;
fileName?:any;
name?:string;
value?:boolean;
}
@@ -1,6 +1,6 @@
export class Tutorial {
isSelected?: boolean;
id?: any;
fileName?: any;
title?: string;
description?: string;
published?: boolean;
+1 -1
View File
@@ -1,5 +1,5 @@
export class User {
id?:any;
fileName?:any;
username?:string;
email?:string;
password?:string;
@@ -14,7 +14,7 @@ export class AdminModuleService {
}
updateUserRoles(id: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${id}`, data);
updateUserRoles(fileName: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${fileName}`, data);
}
}
@@ -1,14 +1,11 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit} from '@angular/core';
import {Router, RouterOutlet} from "@angular/router";
import {DOCUMENT} from '@angular/common';
import { ScrollPanelModule } from 'primeng/scrollpanel';
import {MenuItem} from 'primeng/api';
import {SideBarAdminComponent} from '../side-bar-admin.component/side-bar-admin.component';
@Component({
selector: 'app-admin.component',
imports: [
RouterOutlet,
ScrollPanelModule,
SideBarAdminComponent
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
@@ -19,9 +16,7 @@ export class AdminComponent implements OnInit {
private document = inject<Document>(DOCUMENT);
height = 0;
items: MenuItem[] | undefined;
constructor(private router: Router) {
constructor(private router: Router) {
this.height = 0
}
@@ -66,30 +61,7 @@ export class AdminComponent implements OnInit {
else
this.height = 0;
this.items = [
{
label: 'Admin',
icon: 'pi pi-palette',
command: () => {
this.router.navigate(['/main/admin/welcome']);
},
items: [
{
label: 'Settings',
icon: 'pi pi-eraser',
// route: '/main/admin/settings'
command: () => {
this.router.navigate(['/main/admin/settings']);
}
},
{
label: 'System',
icon: 'pi pi-heart',
route: '/main/admin/system'
}
]
}
];
}
}
@@ -1,15 +1,13 @@
import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core';
import {mainRouting} from '../main-module/main.routing';
import {MainComponent} from '../main-module/main.component/main.component';
import {RouterModule} from '@angular/router';
import {AdminComponent} from './admin.component/admin.component';
import {adminRouting} from './admin.routing';
import { ScrollPanelModule } from 'primeng/scrollpanel';
@NgModule({
declarations: [
],
imports: [adminRouting, AdminComponent,ScrollPanelModule],
imports: [adminRouting, AdminComponent],
exports: [RouterModule],
schemas:[CUSTOM_ELEMENTS_SCHEMA]
})
@@ -85,7 +85,7 @@ export class UsersComponent {
}
saveUserRoles(user:User){
this.adminService.updateUserRoles(user.id, user).subscribe(
this.adminService.updateUserRoles(user.fileName, user).subscribe(
(data:any) => {
console.log(data);
user.isEdit = false
@@ -98,13 +98,13 @@ export class UsersComponent {
isChecked(element: User, role: Role) {
let userRoles: Role[] = element.roles;
return element.roles.some((r:Role) => r.id === role.id);
return element.roles.some((r:Role) => r.fileName === role.fileName);
}
onValChange(element:User, role: Role) {
let userRoles: Role[] = element.roles;
if (this.isChecked(element, role)) {
userRoles = userRoles.filter((r:Role) => r.id !== role.id);
userRoles = userRoles.filter((r:Role) => r.fileName !== role.fileName);
}else {
userRoles.push(role);
}
@@ -29,9 +29,9 @@
<mat-toolbar class="image-toolbar">
<mat-toolbar-row>
<span class="menu-spacer"></span>
<!-- <button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(image.url)">
<button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(image.url)">
<mat-icon>download</mat-icon>
</button>-->
</button>
@if (isLoggedIn) {
<button matMiniFab matTooltip="Save to server" aria-label="Save" (click)="save(image.url)" >
<mat-icon>upload_file</mat-icon>
@@ -19,7 +19,7 @@
color: white;
position: absolute;
left : 50%;
top: 5%;
top: 42px;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
padding: 0px 2px;
@@ -89,6 +89,18 @@ export class GenerateImageComponent {
console.log(error);
});
}
downloadImage(url: string | undefined) {
const link = document.createElement('a');
link.setAttribute('target', '_blank');
link.setAttribute('download', 'image.png');
// @ts-ignore
link.download = url;
// @ts-ignore
link.href = url;
link.click();
}
}
@@ -19,12 +19,12 @@ export class ModeratorApiService {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${id}`);
getTutorial(fileName: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${fileName}`);
}
publish(id: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${id}`, data);
publish(fileName: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${fileName}`, data);
}
}
@@ -55,18 +55,18 @@ const language = 'typescript';
### Blockquote
> Blockquote to the max`;
private id: string | null | undefined;
private fileName: string | null | undefined;
constructor(private moderatorApiService: ModeratorApiService, private route: ActivatedRoute) {
this.route.queryParams
.subscribe(params => {
console.log(params);
this.id = params['id'];
console.log(this.id);
this.fileName = params['fileName'];
console.log(this.fileName);
});
this.moderatorApiService.getTutorial(this.id).subscribe(
this.moderatorApiService.getTutorial(this.fileName).subscribe(
data=>{
this.tutorial = data;
}
@@ -75,7 +75,7 @@ const language = 'typescript';
publishTutorial(): void {
this.tutorial.published = true;
this.moderatorApiService.publish(this.id, this.tutorial)
this.moderatorApiService.publish(this.fileName, this.tutorial)
.subscribe(
response => {
console.log(response);
@@ -9,7 +9,7 @@
@case('isEdit') {
<div class="btn-edit" >
<button mat-button routerLink='../tutorial-preview' [queryParams]="{id:element.id}" (click)="element.isEdit = !element.isEdit">
<button mat-button routerLink='../tutorial-preview' [queryParams]="{fileName:element.fileName}" (click)="element.isEdit = !element.isEdit">
Preview
</button>
@@ -78,7 +78,7 @@ export class TutorialsListComponent {
publish(element: any, checked: boolean){
element.published = checked;
this.moderatorApiService.publish(element.id, element).subscribe(
this.moderatorApiService.publish(element.fileName, element).subscribe(
response => {
console.log(response);
},
@@ -1,5 +1,5 @@
<div class="tutorial-view">
<img style="width: 600px; height: 500px;" src="{{tutorial.titleimage}}" alt="">
<img style="width: 716px; height: 400px;" src="{{tutorial.titleimage}}" alt="">
<markdown class="tutorial-body" [data]="tutorial.body"></markdown>
</div>
@@ -14,7 +14,7 @@ import {MarkdownComponent} from 'ngx-markdown';
styleUrl: './tutorial-view.component.scss'
})
export class TutorialViewComponent {
private id: string | null | undefined;
private fileName: string | null | undefined;
tutorial: Tutorial = new Tutorial();
constructor(private tutorialsApiService: TutorialsApiService, private route: ActivatedRoute) {
@@ -22,11 +22,11 @@ export class TutorialViewComponent {
this.route.queryParams
.subscribe(params => {
console.log(params);
this.id = params['id'];
console.log(this.id);
this.fileName = params['fileName'];
console.log(this.fileName);
});
this.tutorialsApiService.getTutorial(this.id).subscribe(
this.tutorialsApiService.getTutorial(this.fileName).subscribe(
data => {
this.tutorial = data;
}
@@ -17,6 +17,6 @@ export class TutorialsApiService {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${id}`);
getTutorial(fileName: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${fileName}`);
}}
@@ -4,7 +4,7 @@
<img mat-card-image [src]="tutorial.titleimage" alt="Photo of a" />
<mat-card-actions>
<button mat-button routerLink="../tutorial-view" [queryParams]="{id:tutorial.id}" target="_blank">{{tutorial.title}}</button>
<button mat-button routerLink="../tutorial-view" [queryParams]="{fileName:tutorial.fileName}" target="_blank">{{tutorial.title}}</button>
</mat-card-actions>
</mat-card>
@@ -4,12 +4,13 @@
.container-card-view{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(358px, 0fr));
gap: 24px;
width: 100%;
height: 200px;
object-fit: cover;
padding: 24px;
justify-content: center;
//width: 100%;
//height: 100%;
//padding-top: 20px;
@@ -26,13 +27,14 @@
}
img {
width: 100%;
width: 358px;
height: 200px;
object-fit: cover;
}
.responsive-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(358px, 0fr));
gap: 24px;
}
@@ -0,0 +1,19 @@
<p>dialog-cropp-image.component works!</p>
<mat-dialog-content class="mat-typography">
<image-cropper
[imageURL]="image"
style="width: 400px; height: 300px;"
[imageChangedEvent]="imageChangedEvent"
[maintainAspectRatio]="true"
[aspectRatio]="4 / 3"
format="png"
(imageCropped)="imageCropped($event)"
(imageLoaded)="imageLoaded($event)"
(cropperReady)="cropperReady()"
(loadImageFailed)="loadImageFailed()"
></image-cropper>
<img style="width: 358px; height: 200px" [src]="croppedImage" />
</mat-dialog-content>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DialogCroppImageComponent } from './dialog-cropp-image.component';
describe('DialogCroppImageComponent', () => {
let component: DialogCroppImageComponent;
let fixture: ComponentFixture<DialogCroppImageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DialogCroppImageComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DialogCroppImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,62 @@
import {Component, inject, input} from '@angular/core';
import {ImageCroppedEvent, ImageCropperComponent, LoadedImage} from 'ngx-image-cropper';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {UserApiService} from '../user-api.service';
import {MAT_DIALOG_DATA, MatDialogContent} from '@angular/material/dialog';
@Component({
selector: 'app-dialog-cropp-image.component',
imports: [
ImageCropperComponent,
MatDialogContent
],
templateUrl: './dialog-cropp-image.component.html',
styleUrl: './dialog-cropp-image.component.scss'
})
export class DialogCroppImageComponent {
imageChangedEvent: Event | null = null;
croppedImage: SafeUrl = '';
image?: string;
imageCropUrl: string | undefined;
constructor(
private userApiService: UserApiService,
private sanitizer: DomSanitizer,
) {
this.cropImage(this.imageCropUrl);
}
///------------
fileChangeEvent(event: Event): void {
this.imageChangedEvent = event;
}
imageCropped(event: ImageCroppedEvent) {
if (event.objectUrl != null) {
this.croppedImage = this.sanitizer.bypassSecurityTrustUrl(event.objectUrl);
}
// event.blob can be used to upload the cropped image
}
imageLoaded(image: LoadedImage) {
// show cropper
}
cropperReady() {
// cropper ready
}
loadImageFailed() {
// show message
}
cropImage(url: string | undefined) {
if(url !== undefined){
this.userApiService.loadImageAsBase64(url).subscribe(data => {
this.image = data;
})
}
}
}
@@ -1,25 +1,122 @@
<p>images.component works!</p>
<div class="generate-image-view">
@for (fileInfo of fileInfos; track fileInfo) {
<mat-card>
<mat-card-header>
<div class="image-container">
<img src="{{fileInfo.url}}"
alt="" width="100%" height="100%">
<mat-toolbar class="image-toolbar">
<mat-toolbar-row>
<label class="image-file-name">{{fileInfo.name}}</label>
<span class="menu-spacer"></span>
<button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(fileInfo.url)">
<mat-icon>download</mat-icon>
</button>
@if(fileInfo.mode !=="CropView"){
<img src="{{fileInfo.url}}"
alt="" width="100%" height="100%"/>
}@else{
<image-cropper
output = "base64"
[imageURL]="fileInfo.base64string"
style="width: 100%; height: 100%;"
[imageChangedEvent]="imageChangedEvent"
[maintainAspectRatio]="true"
[aspectRatio]="4 / 3"
[canvasRotation]="canvasRotation"
[cropperStaticWidth]="cropperStaticWidth"
[cropperStaticHeight]="cropperStaticHeight"
[(transform)]="transform"
format="png"
(imageCropped)="imageCropped($event)"
(imageLoaded)="imageLoaded($event)"
(cropperReady)="cropperReady()"
(loadImageFailed)="loadImageFailed()"
></image-cropper>
</mat-toolbar-row>
</mat-toolbar>
</div>
<img #imagePreview width="100%" height="100%" [src]="croppedImage" />
}
@if(fileInfo.mode !=="CropView"){
<mat-toolbar class="image-toolbar">
<mat-toolbar-row>
<label class="image-file-name">{{fileInfo.name}}</label>
<span class="menu-spacer"></span>
<button matMiniFab matTooltip="Crop image" aria-label="Crop" (click)="cropImage(fileInfo)">
<mat-icon>photo_size_select_small</mat-icon>
</button>
<button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(fileInfo.url)">
<mat-icon>download</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
}@else{
<mat-toolbar class="crop-toolbar">
<!-- Zoom -->
<mat-toolbar-row>
<button matMiniFab matTooltip="Size +" aria-label="Crop" (click)="zoomIn()">
<mat-icon>zoom_in</mat-icon>
</button>
<button matMiniFab matTooltip="Size -" aria-label="Download" (click)="zoomOut()">
<mat-icon>zoom_out</mat-icon>
</button>
</mat-toolbar-row>
<!-- Move -->
<mat-toolbar-row class="move-toolbar-row">
<table class="move-toolbar-table">
<tr style="display: flex; justify-content: center;">
<th colspan="2" >
<button matMiniFab matTooltip="Move up" aria-label="Crop" (click)="moveUp()">
<mat-icon>north</mat-icon>
</button>
</th>
</tr>
<tr>
<td>
<button matMiniFab matTooltip="Move left" aria-label="Download" (click)="moveLeft()">
<mat-icon>west</mat-icon>
</button>
</td>
<td>
<button matMiniFab matTooltip="Move right" aria-label="Crop" (click)="moveRight()">
<mat-icon>east</mat-icon>
</button>
</td>
</tr>
<tr style="display: flex; justify-content: center;">
<th colspan="2" >
<button matMiniFab matTooltip="Move down" aria-label="Download" (click)="moveDown()">
<mat-icon>south</mat-icon>
</button>
</th>
</tr>
</table>
</mat-toolbar-row>
<!-- Rotate -->
<mat-toolbar-row>
<button matMiniFab matTooltip="Rotate left" aria-label="Crop" (click)="rotateLeft()">
<mat-icon>rotate_left</mat-icon>
</button>
<button matMiniFab matTooltip="Rotate right" aria-label="Download" (click)="rotateRight()">
<mat-icon>rotate_right</mat-icon>
</button>
</mat-toolbar-row>
<!-- Flip -->
<mat-toolbar-row>
<button matMiniFab matTooltip="Flip vertical" aria-label="Crop" (click)="flipHorizontal()">
<mat-icon>flip</mat-icon>
</button>
<button matMiniFab matTooltip="Flip horizontal" aria-label="Download" (click)="flipVertical()">
<mat-icon style="transform: rotate(90deg)">flip</mat-icon>
</button>
</mat-toolbar-row>
<mat-toolbar-row>
<mat-chip-listbox class="mat-mdc-chip-set-stacked" aria-label="Cutest dog breeds">
<mat-chip-option (selectionChange)="cropFixedSize($event)" >Crop 716x400</mat-chip-option>
</mat-chip-listbox>
<span class="menu-spacer"></span>
</mat-toolbar-row>
<mat-toolbar-row>
<button matMiniFab matTooltip="Save to server" aria-label="Save" (click)="save()" >
<mat-icon>upload_file</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
}
</div>
</mat-card-header>
<mat-card-content>
</mat-card-content>
@@ -11,13 +11,15 @@
color: white;
position: absolute;
left : 50%;
top: 5%;
top: 47px;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
padding: 0px 2px;
border: none;
border-radius: 5px;
width: 100%;
}
.image-container .menu-spacer {
flex: 1 1 auto;
}
@@ -29,3 +31,33 @@
.mat-mdc-mini-fab{
margin: 5px;
}
.spacer {
flex: 1 1 auto;
}
.move-toolbar-row{
height: 140px;
}
.image-container .crop-toolbar{
background-color: rgba(153,153,153,0);
color: white;
position: absolute;
left : 59px;
top: 240px;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
padding: 0px 2px;
border: none;
border-radius: 5px;
width: 10%;
}
.move-toolbar-table{
display: flex;
justify-content: center;
flex-direction: column;
}
@@ -1,12 +1,16 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, inject} from '@angular/core';
import {Component, ElementRef, inject, ViewChild} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader} from '@angular/material/card';
import {MatMiniFabButton} from '@angular/material/button';
import {MatToolbar, MatToolbarRow} from '@angular/material/toolbar';
import {MatTooltip} from '@angular/material/tooltip';
import {MatIcon} from '@angular/material/icon';
import {FileInfo} from '../../../models/file-info';
import {UserApiService} from '../user-api.service';
import {Base64ImagePayload, UploadImageResponse, UserApiService} from '../user-api.service';
import {CropService, ImageCroppedEvent, ImageCropperComponent, ImageTransform, LoadedImage} from 'ngx-image-cropper';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {MatDialog} from '@angular/material/dialog';
import {DialogCroppImageComponent} from '../dialog-cropp-image.component/dialog-cropp-image.component';
import {MatChipListbox, MatChipOption, MatChipSelectionChange} from '@angular/material/chips';
@Component({
selector: 'app-images.component',
@@ -18,17 +22,49 @@ import {MatDialog} from '@angular/material/dialog';
MatMiniFabButton,
MatToolbar,
MatToolbarRow,
MatTooltip
MatTooltip,
ImageCropperComponent,
MatChipListbox,
MatChipOption
],
templateUrl: './images.component.html',
styleUrl: './images.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
styleUrl: './images.component.scss'
})
export class ImagesComponent {
fileInfos?: FileInfo[] = [];
readonly dialog = inject(MatDialog);
constructor(private userApiService: UserApiService) {
imageChangedEvent: Event | null = null;
croppedImage: string | null | undefined = '';
croppedImageBase64: string | null | undefined;
@ViewChild('imagePreview') imagePreview!: ElementRef;
canvasRotation = 0;
transform: ImageTransform = {
translateUnit: 'px',
scale: 1,
rotate: 0,
flipH: false,
flipV: false,
translateH: 0,
translateV: 0
};
cropperStaticWidth = 0;
cropperStaticHeight = 0;
loading = false;
currentCroppedFile: FileInfo | undefined;
constructor(
private userApiService: UserApiService,
private sanitizer: DomSanitizer,
) {
this.userApiService.getImages().subscribe(data =>{
this.fileInfos = data;
console.log(data);
@@ -36,6 +72,214 @@ export class ImagesComponent {
}
downloadImage(url: string | undefined) {
const link = document.createElement('a');
// @ts-ignore
link.download = url;
// @ts-ignore
link.href = url;
link.click();
}
openCropImageDialog(imageUrl: string | undefined, enterAnimationDuration: string, exitAnimationDuration: string) {
let dialogCropImageRef = this.dialog.open(DialogCroppImageComponent, {
height: '500px',
width: '800px',
enterAnimationDuration,
exitAnimationDuration,
}
);
dialogCropImageRef.componentInstance.cropImage(imageUrl);
// dialogCropImageRef.componentInstance.uploadClicked.subscribe(result => {
// dialogCropImageRef.close();
// this.openUploadImageDialog(enterAnimationDuration, exitAnimationDuration);
// })
// const dialogSelectSubscription = dialogCropImageRef.componentInstance.selectClicked
// .subscribe(result => {
// console.log('Got the data!', result);
//
// if (result == null) {
// return;
// }
// this.tutorial.titleimage = result.url;
//
// });
}
cropImage(fileInfo: FileInfo) {
// close the crop view for a previous cropped file
if(this.currentCroppedFile !== undefined){
this.currentCroppedFile.mode = "View";
}
fileInfo.mode = "CropView";
this.setBase64String(fileInfo);
this.currentCroppedFile = fileInfo;
}
setBase64String(fileInfo:FileInfo) {
if(fileInfo.url !== undefined){
this.userApiService.loadImageAsBase64(fileInfo.url).subscribe(data => {
fileInfo.base64string = data;
})
}
}
imageCropped(event: ImageCroppedEvent) {
if (event.objectUrl != null) {
// this.croppedImage = this.sanitizer.bypassSecurityTrustUrl(event.objectUrl);
}
else {
this.croppedImageBase64 = event.base64;
this.croppedImage = this.croppedImageBase64;
}
this.imagePreview.nativeElement.height = event.height;
this.imagePreview.nativeElement.width = event.width;
}
imageLoaded(image: LoadedImage) {
// show cropper
}
cropperReady() {
// cropper ready
}
loadImageFailed() {
// show message
}
zoomIn() {
this.transform = {
...this.transform,
scale: this.transform.scale! + .1
};
}
zoomOut() {
this.transform = {
...this.transform,
scale: this.transform.scale! - .1
};
}
moveLeft() {
this.transform = {
...this.transform,
translateH: this.transform.translateH! - 1
};
}
moveRight() {
this.transform = {
...this.transform,
translateH: this.transform.translateH! + 1
};
}
moveDown() {
this.transform = {
...this.transform,
translateV: this.transform.translateV! + 1
};
}
moveUp() {
this.transform = {
...this.transform,
translateV: this.transform.translateV! - 1
};
}
flipHorizontal() {
this.transform = {
...this.transform,
flipH: !this.transform.flipH
};
}
flipVertical() {
this.transform = {
...this.transform,
flipV: !this.transform.flipV
};
}
rotateLeft() {
this.loading = true;
setTimeout(() => { // Use timeout because rotating image is a heavy operation and will block the ui thread
this.canvasRotation--;
this.flipAfterRotate();
});
}
rotateRight() {
this.loading = true;
setTimeout(() => {
this.canvasRotation++;
this.flipAfterRotate();
});
}
private flipAfterRotate() {
const flippedH = this.transform.flipH;
const flippedV = this.transform.flipV;
this.transform = {
...this.transform,
flipH: flippedV,
flipV: flippedH,
translateH: 0,
translateV: 0
};
}
cropFixedSize($event: MatChipSelectionChange) {
if ($event.source.selected) {
this.cropperStaticWidth = 716;
this.cropperStaticHeight = 400;
}
else {
this.cropperStaticWidth = 0;
this.cropperStaticHeight = 0;
}
}
save() {
const payload = this.getBase64ImagePayload();
this.userApiService.uploadUserBase64Image(payload).subscribe({
next: (res:UploadImageResponse) =>{
const fileInfo: FileInfo = {
name: res.fileName,
url: res.url,
mode: "View",
base64string: undefined
}
this.fileInfos?.push(fileInfo);
console.log("Uploaded:", res);
} ,
error: (err) => console.error("Upload failed", err),
});
}
private getBase64ImagePayload(): Base64ImagePayload {
const match = this.croppedImageBase64?.match(/^data:(.+);base64,(.*)$/);
if(match)
{
const [, mime, base64] = match;
return {
mime: mime,
data: base64
};
}
else {
throw new Error("Invalid base64 string");
}
}
}
@@ -70,7 +70,7 @@
<mat-divider></mat-divider>
<mat-tab-group>
<mat-tab label="Markdown text | Result">
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
<textarea class="variable-textarea" [(ngModel)]="tutorial.body"></textarea>
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
</mat-tab>
<mat-tab label="Editor">
@@ -70,7 +70,7 @@ const language = 'typescript';
### Blockquote
> Blockquote to the max`;
private id: string | null | undefined;
private fileName: string | null | undefined;
constructor(private userApiService: UserApiService,
private route: ActivatedRoute,
@@ -81,11 +81,11 @@ const language = 'typescript';
this.route.queryParams
.subscribe(params => {
console.log(params);
this.id = params['id'];
console.log(this.id);
this.fileName = params['fileName'];
console.log(this.fileName);
});
this.userApiService.getTutorial(this.id).subscribe(
this.userApiService.getTutorial(this.fileName).subscribe(
data=>{
this.tutorial = data;
}
@@ -148,7 +148,7 @@ const language = 'typescript';
}
updateTutorial(): void {
this.userApiService.update(this.id, this.tutorial)
this.userApiService.update(this.fileName, this.tutorial)
.subscribe(
response => {
console.log(response);
@@ -37,13 +37,13 @@
}
@case('isEdit') {
<div class="btn-edit" >
<button mat-button routerLink='../tutorial-edit' [queryParams]="{id:element.id}" (click)="element.isEdit = !element.isEdit">
<button mat-button routerLink='../tutorial-edit' [queryParams]="{fileName:element.fileName}" (click)="element.isEdit = !element.isEdit">
Edit
</button>
<button
mat-button
class="button-remove"
(click)="removeRow(element.id)"
(click)="removeRow(element.fileName)"
>
Delete
</button>
@@ -98,7 +98,7 @@
<button
mat-button
class="button-remove"
(click)="removeRow(element.id)"
(click)="removeRow(element.fileName)"
>
Delete
</button>
@@ -116,7 +116,7 @@
<button
mat-button
(click)="editRow(element)"
[disabled]="disableSubmit(element.id)"
[disabled]="disableSubmit(element.fileName)"
>
Done
</button>
@@ -125,17 +125,17 @@ import {MatSlideToggle} from '@angular/material/slide-toggle';
// })
}
removeRow(id: number) {
this.userApiService.deleteTutorial(id).subscribe(() => {
removeRow(fileName: number) {
this.userApiService.deleteTutorial(fileName).subscribe(() => {
this.dataSource.data = this.dataSource.data.filter(
(u: Tutorial) => u.id !== id,
(u: Tutorial) => u.fileName !== fileName,
)
})
}
publish(element: any, checked: boolean){
element.tobepublished = checked;
this.userApiService.update(element.id, element).subscribe(
this.userApiService.update(element.fileName, element).subscribe(
response => {
console.log(response);
},
@@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import {forkJoin, Observable} from 'rxjs';
import {forkJoin, Observable, switchMap} from 'rxjs';
import {Tutorial} from '../../models/tutorial.model';
import {HttpClient, HttpEvent, HttpHeaders, HttpRequest} from '@angular/common/http';
import {GlobalConstants} from '../../global-constants';
import {FileInfo} from '../../models/file-info';
import {map} from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@@ -15,6 +16,11 @@ export class UserApiService {
}
uploadUserBase64Image(base64Image: Base64ImagePayload): Observable<any> {
return this.http.post<UploadImageResponse>(`${this.baseUrl}/user-images/upload-base64-image`, base64Image, { headers: { "Content-Type": "application/json" } }
);
}
upload(file: File): Observable<HttpEvent<any>> {
const formData: FormData = new FormData();
formData.append('file', file);
@@ -36,32 +42,59 @@ export class UserApiService {
return this.http.get<FileInfo[]>(`${this.baseUrl}/getImages`);
}
loadImageAsBase64(imageUrl: string): Observable<string> {
return this.http.post<Base64ImageResponse>(`${this.baseUrl}/user-images/base64-image`, imageUrl)
.pipe(map(r => `data:${r.mime};base64,${(r.data || '').trim()}`));
}
getUserAllTutorials(): Observable<Tutorial[]> {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${id}`);
getTutorial(fileName: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${fileName}`);
}
create(data: any): Observable<any> {
return this.http.post(`${this.baseUrl}/tutorial-add`, data);
}
update(id: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${id}`, data);
update(fileName: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${fileName}`, data);
}
deleteTutorials(tutorials: Tutorial[]): Observable<Tutorial[]> {
return forkJoin(
tutorials.map((tutorial) =>
this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${tutorial.id}`)
this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${tutorial.fileName}`)
)
);
}
deleteTutorial(id: number): Observable<Tutorial> {
return this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${id}`);
deleteTutorial(fileName: number): Observable<Tutorial> {
return this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${fileName}`);
}
}
export interface Base64ImagePayload {
mime: string; // e.g., "image/png"
data: string; // base64 string (no "data:" prefix)
}
export interface UploadImageResponse {
message: string;
fileName: string;
url: string; // URL to access the stored image
mime: string;
size: number; // bytes
}
export interface Base64ImageResponse {
message: string;
mime: string;
data: string; // Base64 without "data:" prefix
}