Merge pull request #65 from liosha84/development

Development
This commit is contained in:
liosha84
2025-08-06 20:40:47 +03:00
committed by GitHub
46 changed files with 861 additions and 281 deletions
-6
View File
@@ -24,8 +24,6 @@
"@angular/platform-server": "^20.0.0", "@angular/platform-server": "^20.0.0",
"@angular/router": "^20.0.0", "@angular/router": "^20.0.0",
"@angular/ssr": "^20.0.2", "@angular/ssr": "^20.0.2",
"@ant-design/icons-angular": "19.0.0",
"@hreimer/angular-image-viewer": "^0.14.1",
"@ng-bootstrap/ng-bootstrap": "19.0.0", "@ng-bootstrap/ng-bootstrap": "19.0.0",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"@primeng/themes": "^19.1.3", "@primeng/themes": "^19.1.3",
@@ -33,11 +31,7 @@
"bootstrap": "5.3.6", "bootstrap": "5.3.6",
"cropperjs": "^2.0.1", "cropperjs": "^2.0.1",
"express": "^5.1.0", "express": "^5.1.0",
"file-saver": "^2.0.5",
"ngx-filesaver": "^20.0.0",
"ngx-markdown": "^20.0.0", "ngx-markdown": "^20.0.0",
"ngx-scrollbar": "18.0.0",
"primeng": "^19.1.3",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
+3 -1
View File
@@ -17,6 +17,7 @@ import {AppRoutingModule} from './app.routes';
import {App} from './app'; import {App} from './app';
import {CustomHttpInterceptor} from './helpers/custom-http-interceptor'; import {CustomHttpInterceptor} from './helpers/custom-http-interceptor';
import {AngularMarkdownEditorModule} from 'angular-markdown-editor'; import {AngularMarkdownEditorModule} from 'angular-markdown-editor';
import {MatFormFieldModule} from '@angular/material/form-field';
@NgModule({ @NgModule({
@@ -33,7 +34,8 @@ import {AngularMarkdownEditorModule} from 'angular-markdown-editor';
AdminWelcomeComponent, AdminWelcomeComponent,
SettingsComponent, SettingsComponent,
SystemComponent, SystemComponent,
AngularMarkdownEditorModule.forRoot({ iconlibrary: 'fa' }) AngularMarkdownEditorModule.forRoot({ iconlibrary: 'fa' }),
MatFormFieldModule
], ],
providers: [ providers: [
authInterceptorProviders,provideHttpClient(withInterceptorsFromDi()),{ authInterceptorProviders,provideHttpClient(withInterceptorsFromDi()),{
@@ -0,0 +1,40 @@
<div>
<mat-form-field>
<div>
<mat-toolbar>
<input matInput [value]="fileName" />
<button
mat-button
color="primary"
[disabled]="!currentFile"
(click)="upload()"
>
Upload
</button>
</mat-toolbar>
<input
type="file"
id="fileInput"
(change)="selectFile($event)"
name="fileInput"
/>
</div>
</mat-form-field>
</div>
@if (progress) {
<mat-toolbar class="progress-bar">
<mat-progress-bar color="accent" [value]="progress"></mat-progress-bar>
<span class="progress">{{ progress }}%</span>
</mat-toolbar>
}
@if (message) {
<div class="message">
{{ message }}
</div>
}
@if (fileInfo){
<img src="{{fileInfo.url}}" alt=""/>
}
@@ -0,0 +1,47 @@
.progress-bar {
padding: 0;
}
.progress {
width: 50px;
}
#fileInput {
position: absolute;
cursor: pointer;
z-index: 10;
opacity: 0;
height: 100%;
left: 0px;
top: 0px;
}
.mat-toolbar-single-row {
height: auto !important;
background: transparent;
padding: 0;
}
.mat-toolbar-single-row button {
width: 100px;
}
.mat-form-field {
width: 100%;
}
.mat-mdc-form-field {
display: block;
}
.message {
background-color: #ddd;
padding: 15px;
color: #333;
border: #aaa solid 1px;
border-radius: 4px;
margin-bottom: 10px;
}
img{
max-width: -webkit-fill-available;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FileUploadComponent } from './file-upload.component';
describe('FileUploadComponent', () => {
let component: FileUploadComponent;
let fixture: ComponentFixture<FileUploadComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FileUploadComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FileUploadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,80 @@
import {Component, EventEmitter, OnInit, Output} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {MatList, MatListItem} from '@angular/material/list';
import {AsyncPipe} from '@angular/common';
import {MatToolbar} from '@angular/material/toolbar';
import {MatProgressBar} from '@angular/material/progress-bar';
import {MatFormField, MatInput} from '@angular/material/input';
import {Observable} from 'rxjs';
import {HttpEventType, HttpResponse} from '@angular/common/http';
import {MatButton} from '@angular/material/button';
import {UserApiService} from '../../modules/user-module/user-api.service';
import {FileInfo} from '../../models/file-info';
@Component({
selector: 'app-file-upload',
imports: [
MatToolbar,
MatProgressBar,
MatFormField,
MatButton,
MatInput
],
templateUrl: './file-upload.component.html',
styleUrl: './file-upload.component.scss'
})
export class FileUploadComponent {
currentFile?: File;
progress = 0;
message = '';
fileName = 'Select File';
@Output() fileInfo: FileInfo | undefined;
@Output() onImageUploaded = new EventEmitter<FileInfo>();
constructor(private userApiService: UserApiService) {}
selectFile(event: any): void {
this.progress = 0;
this.message = '';
if (event.target.files && event.target.files[0]) {
const file: File = event.target.files[0];
this.currentFile = file;
this.fileName = this.currentFile.name;
} else {
this.fileName = 'Select File';
}
}
upload(): void {
if (this.currentFile) {
this.userApiService.upload(this.currentFile).subscribe({
next: (event: any) => {
if (event.type === HttpEventType.UploadProgress) {
this.progress = Math.round((100 * event.loaded) / event.total);
} else if (event instanceof HttpResponse) {
this.message = event.body.message;
this.fileInfo =event.body.fileInfo;
this.onImageUploaded.emit(this.fileInfo);
console.log(event.body);
}
},
error: (err: any) => {
console.log(err);
this.progress = 0;
if (err.error && err.error.message) {
this.message = err.error.message;
} else {
this.message = 'Could not upload the file!';
this.onImageUploaded.emit(undefined);
}
},
complete: () => {
this.currentFile = undefined;
},
});
}
}
}
@@ -19,9 +19,9 @@ export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
req = req.clone({ // req = req.clone({
withCredentials: true, // withCredentials: true,
}); // });
return next.handle(req).pipe( return next.handle(req).pipe(
catchError((error) => { catchError((error) => {
@@ -1,4 +1,4 @@
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http'; import {HttpEvent, HttpEventType, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
import {Observable, tap} from 'rxjs'; import {Observable, tap} from 'rxjs';
import {SpinnerService} from '../services/spinner.service'; import {SpinnerService} from '../services/spinner.service';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
@@ -19,6 +19,7 @@ export class CustomHttpInterceptor implements HttpInterceptor {
} }
}, (error) => { }, (error) => {
this.spinnerService.hide(); this.spinnerService.hide();
})); }))
;
} }
} }
@@ -1,33 +1,23 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, Inject, inject, Renderer2} from '@angular/core'; import {Component, CUSTOM_ELEMENTS_SCHEMA, inject} from '@angular/core';
import {AsyncPipe, DOCUMENT, NgOptimizedImage} from "@angular/common"; import {AsyncPipe} from "@angular/common";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {MatButton, MatMiniFabButton} from "@angular/material/button"; import {MatButton, MatMiniFabButton} from "@angular/material/button";
import { import {
MatCard, MatCard,
MatCardActions, MatCardActions,
MatCardContent, MatCardContent,
MatCardHeader, MatCardHeader
MatCardImage,
MatCardXlImage
} from "@angular/material/card"; } from "@angular/material/card";
import {MatFormField, MatInput, MatLabel} from "@angular/material/input"; import {MatFormField, MatInput, MatLabel} from "@angular/material/input";
import {Image} from '../../../models/image'; import {Image} from '../../../models/image';
import {ZhipuaiImageService} from '../zhipuai-image.service'; import {ZhipuaiImageService} from '../zhipuai-image.service';
import {SpinnerService} from '../../../services/spinner.service'; import {SpinnerService} from '../../../services/spinner.service';
import {MatProgressSpinner} from '@angular/material/progress-spinner'; import {MatProgressSpinner} from '@angular/material/progress-spinner';
import {FileUploadService} from '../../../services/file-upload-api.service';
import {Observable, Subscription} from 'rxjs';
import {HttpClient, HttpResponse} from '@angular/common/http';
import {MatToolbar, MatToolbarRow} from '@angular/material/toolbar'; import {MatToolbar, MatToolbarRow} from '@angular/material/toolbar';
import {MatIcon} from '@angular/material/icon'; import {MatIcon} from '@angular/material/icon';
import {MatTooltip} from '@angular/material/tooltip'; import {MatTooltip} from '@angular/material/tooltip';
import {AuthService} from '../../../services/auth.service';
import {TokenStorageService} from '../../../services/token-storage.service'; import {TokenStorageService} from '../../../services/token-storage.service';
import {FileInfo} from '../../../models/file-info';
import {UserApiService} from '../../user-module/user-api.service'; import {UserApiService} from '../../user-module/user-api.service';
import {MatProgressBar} from '@angular/material/progress-bar';
@Component({ @Component({
selector: 'app-generate-image.component', selector: 'app-generate-image.component',
@@ -43,26 +33,17 @@ import {MatProgressBar} from '@angular/material/progress-bar';
MatInput, MatInput,
MatLabel, MatLabel,
MatProgressSpinner, MatProgressSpinner,
NgOptimizedImage,
MatCardXlImage,
MatCardImage,
MatToolbarRow, MatToolbarRow,
MatToolbar, MatToolbar,
MatIcon, MatIcon,
MatMiniFabButton, MatMiniFabButton,
MatTooltip, MatTooltip,
MatProgressBar
], ],
templateUrl: './generate-image.component.html', templateUrl: './generate-image.component.html',
styleUrl: './generate-image.component.scss', styleUrl: './generate-image.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class GenerateImageComponent { export class GenerateImageComponent {
isSpinnerVisible = false;
query : string = ''; query : string = '';
images: Image[] = []; images: Image[] = [];
@@ -70,10 +51,6 @@ export class GenerateImageComponent {
url: '' url: ''
}; };
currentFile?: File;
message = '';
isLoggedIn = false; isLoggedIn = false;
private storageService: TokenStorageService = inject(TokenStorageService); private storageService: TokenStorageService = inject(TokenStorageService);
@@ -81,13 +58,8 @@ export class GenerateImageComponent {
constructor( constructor(
private zhipuaiImageService: ZhipuaiImageService, private zhipuaiImageService: ZhipuaiImageService,
public spinnerService: SpinnerService, public spinnerService: SpinnerService,
private uploadService: FileUploadService, private userApiService:UserApiService
private userApiService:UserApiService,
) { ) {
this.isLoggedIn = this.storageService.isLoggedIn(); this.isLoggedIn = this.storageService.isLoggedIn();
} }
@@ -108,7 +80,6 @@ export class GenerateImageComponent {
) )
} }
save(url: string | undefined) { save(url: string | undefined) {
let requestUrl:string = url?url:""; let requestUrl:string = url?url:"";
console.log(requestUrl); console.log(requestUrl);
@@ -118,9 +89,6 @@ export class GenerateImageComponent {
console.log(error); console.log(error);
}); });
} }
} }
@@ -2,13 +2,15 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {tutorialsRouting} from './tutorials.routing'; import {tutorialsRouting} from './tutorials.routing';
import {TutorialsComponent} from './tutorials.component/tutorials.component'; import {TutorialsComponent} from './tutorials.component/tutorials.component';
import {MatFormFieldModule} from '@angular/material/form-field';
@NgModule({ @NgModule({
declarations: [], declarations: [],
imports: [ imports: [
tutorialsRouting, tutorialsRouting,
TutorialsComponent, TutorialsComponent,
CommonModule CommonModule,
MatFormFieldModule
] ]
}) })
export class TutorialsModule { } export class TutorialsModule { }
@@ -1,5 +1,5 @@
<h1 mat-dialog-title>Hi </h1> <h1 mat-dialog-title>Hi </h1>
<div mat-dialog-content > <!--<div mat-dialog-content >-->
<mat-dialog-content class="mat-typography"> <mat-dialog-content class="mat-typography">
<!-- <mat-nav-list>--> <!-- <mat-nav-list>-->
@for (fileInfo of fileInfos; track fileInfo){ @for (fileInfo of fileInfos; track fileInfo){
@@ -16,7 +16,7 @@
<!-- </mat-nav-list>--> <!-- </mat-nav-list>-->
</mat-dialog-content> </mat-dialog-content>
</div> <!--</div>-->
<div mat-dialog-actions> <div mat-dialog-actions>
<button mat-button (click)="upload()">Upload</button> <button mat-button (click)="upload()">Upload</button>
<span class="menu-spacer"></span> <span class="menu-spacer"></span>
@@ -2,20 +2,30 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.mat-dialog-content{ .mat-mdc-list-item{
}
a.mdc-list-item
{
cursor: grab;
height: 120px;
padding-bottom: 66px;
background-color: rgba(24,255,255,0.04);
}
/*.mat-dialog-content{
min-height: 300px; min-height: 300px;
min-width: 300px; min-width: 300px;
height: 75%; height: 75%;
width: 75%; width: 75%;
} }*/
.mdc-dialog--open .mat-mdc-dialog-inner-container /*.mdc-dialog--open .mat-mdc-dialog-inner-container
{ {
opacity: 1; opacity: 1;
width: 600px; width: 600px;
} }*/
.mat-mdc-dialog-container { /*.mat-mdc-dialog-container {
width: 600px; width: 600px;
height: 500px; height: 500px;
display: block; display: block;
@@ -25,4 +35,9 @@
min-width: inherit; min-width: inherit;
max-width: inherit; max-width: inherit;
outline: 0; outline: 0;
}*/
/*
.mat-mdc-dialog-content{
overflow: auto;
} }
*/
@@ -7,8 +7,7 @@ import {
MatDialogTitle MatDialogTitle
} from '@angular/material/dialog'; } from '@angular/material/dialog';
import {MatButton} from '@angular/material/button'; import {MatButton} from '@angular/material/button';
import {MatListItem, MatNavList} from '@angular/material/list'; import {MatListItem} from '@angular/material/list';
import {RouterLink} from '@angular/router';
import {FileInfo} from '../../../models/file-info'; import {FileInfo} from '../../../models/file-info';
import {UserApiService} from '../user-api.service'; import {UserApiService} from '../user-api.service';
@@ -20,10 +19,7 @@ import {UserApiService} from '../user-api.service';
MatButton, MatButton,
MatDialogActions, MatDialogActions,
MatDialogClose, MatDialogClose,
MatListItem
MatListItem,
MatNavList,
], ],
templateUrl: './dialog-select-image.component.html', templateUrl: './dialog-select-image.component.html',
styleUrl: './dialog-select-image.component.scss' styleUrl: './dialog-select-image.component.scss'
@@ -42,10 +38,10 @@ export class DialogSelectImageComponent {
console.log(data); console.log(data);
}); });
} }
//open upload dialog
upload() { upload() {
this.uploadClicked.emit(); this.uploadClicked.emit();
} }
onNoClick() { onNoClick() {
@@ -54,5 +50,6 @@ export class DialogSelectImageComponent {
select(fileInfo:FileInfo) { select(fileInfo:FileInfo) {
this.selectClicked.emit(fileInfo); this.selectClicked.emit(fileInfo);
this.dialogRef.close();
} }
} }
@@ -0,0 +1,14 @@
<p>dialog-upload-image.component works!</p>
<mat-dialog-content class="mat-typography">
<app-file-upload #imageUpload (onImageUploaded)="imageUpload_ImageUploaded($event)"></app-file-upload>
</mat-dialog-content>
<div mat-dialog-actions>
<button mat-button (click)="openSelectImageDialog()">Select image</button>
<span class="menu-spacer"></span>
<button mat-button mat-dialog-close (click)="onNoClick()">No Thanks</button>
<!-- <button mat-button cdkFocusInitial (click)="select()">Ok</button>-->
<button mat-button cdkFocusInitial [disabled]="!imageUploaded" (click)="selectUploadedImage()">Ok</button>
</div>
@@ -0,0 +1,6 @@
.menu-spacer {
flex: 1 1 auto;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DialogUploadImageComponent } from './dialog-upload-image.component';
describe('DialogUploadImageComponent', () => {
let component: DialogUploadImageComponent;
let fixture: ComponentFixture<DialogUploadImageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DialogUploadImageComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DialogUploadImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,54 @@
import {Component, EventEmitter, inject, Output, ViewChild} from '@angular/core';
import {FileUploadComponent} from '../../../components/file-upload.component/file-upload.component';
import {MatDialogActions, MatDialogClose, MatDialogContent, MatDialogRef} from '@angular/material/dialog';
import {MatButton} from '@angular/material/button';
import {FileInfo} from '../../../models/file-info';
@Component({
selector: 'app-dialog-upload-image.component',
imports: [
FileUploadComponent,
MatDialogContent,
MatButton,
MatDialogActions,
MatDialogClose
],
templateUrl: './dialog-upload-image.component.html',
styleUrl: './dialog-upload-image.component.scss'
})
export class DialogUploadImageComponent {
//select uploaded file
@Output() selectUploadedImageClicked = new EventEmitter<any>();
//open select dialog
@Output() openSelectDialogClicked = new EventEmitter<any>();
readonly dialogRef = inject(MatDialogRef<DialogUploadImageComponent>);
@ViewChild('imageUpload') imageUpload: FileUploadComponent | undefined;
imageUploaded:boolean = false;
onNoClick() {
this.dialogRef.close();
}
selectUploadedImage() {
this.selectUploadedImageClicked.emit(this.imageUpload?.fileInfo);
this.dialogRef.close();
}
openSelectImageDialog() {
this.openSelectDialogClicked.emit();
this.dialogRef.close();
}
imageUpload_ImageUploaded($event: FileInfo | undefined) {
if($event){
this.imageUploaded = true;
}
else{
this.imageUploaded = false;
}
}
}
@@ -4,38 +4,55 @@
</mat-card-header> </mat-card-header>
<mat-card-content > <mat-card-content >
<mat-form-field class="example-full-width"> <mat-form-field class="example-full-width">
<mat-label>Title</mat-label> <mat-label class="label-style">Title</mat-label>
<input matInput required <input matInput required
[(ngModel)]="tutorial.title"> [(ngModel)]="tutorial.title">
</mat-form-field> </mat-form-field>
<mat-form-field class="example-full-width">
<mat-label>Use title image</mat-label>
<img style="width: 120px; height: 120px" src="{{tutorial.titleimage}}" alt="">
<input matInput
[(ngModel)]="tutorial.titleimage">
<button mat-raised-button color="primary" (click)="openSelectImageDialog('100ms', '5ms')">Select image</button>
</mat-form-field>
<div class="example-full-width">
<mat-label>Description</mat-label>
<mat-tab-group>
<mat-tab label="Markdown text">
<div class="markdown-editor-container">
<div class="markdown-editor">
<form novalidate>
<angular-markdown-editor
textareaId="editor1"
[options]="editorOptions"
name="markdownText"
[(ngModel)]="tutorial.description"
(onFullscreenExit)="hidePreview()"
>
</angular-markdown-editor>
</form>
<mat-label class="label-style">Title image</mat-label>
<mat-divider></mat-divider>
<table class="example-full-width">
<tr>
<td style="width: 120px;">
<img style="width: 120px; height: 120px" src="{{tutorial.titleimage}}" alt="">
</td>
<td>
<mat-form-field class="example-full-width">
<mat-label>Title image</mat-label>
<input matInput disabled
[(ngModel)]="tutorial.titleimage">
</mat-form-field>
<div>
<button mat-raised-button color="primary" (click)="openSelectImageDialog('100ms', '5ms')">Select image</button>
<button mat-raised-button color="primary" (click)="openUploadImageDialog('100ms', '5ms')">Upload image</button>
</div> </div>
</td>
</tr>
</table>
<div class="example-full-width">
<mat-label class="label-style">Description</mat-label>
<mat-divider></mat-divider>
<mat-tab-group>
<mat-tab label="Markdown text | Result">
<textarea class="variable-textarea" [(ngModel)]="tutorial.description"></textarea>
<markdown class="variable-binding" [data]="tutorial.description"></markdown> <markdown class="variable-binding" [data]="tutorial.description"></markdown>
</mat-tab>
<mat-tab label="Editor">
<div class="markdown-editor-container">
<div class="markdown-editor">
<form novalidate>
<angular-markdown-editor
textareaId="editor1"
[options]="editorOptions"
name="markdownText"
[(ngModel)]="tutorial.description"
(onFullscreenExit)="hidePreview()"
>
</angular-markdown-editor>
</form>
</div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="Result"> <mat-tab label="Result">
@@ -47,26 +64,26 @@
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
<div class="example-full-width">
<mat-label>Body</mat-label>
<mat-tab-group>
<mat-tab label="Markdown text">
<div class="markdown-editor-container">
<div class="markdown-editor">
<form novalidate>
<angular-markdown-editor
textareaId="editor2"
[options]="editorOptions"
name="markdownText"
[(ngModel)]="tutorial.body"
(onFullscreenExit)="hidePreview()"
>
</angular-markdown-editor>
</form>
</div>
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
</div>
<div class="example-full-width">
<mat-label class="label-style">Body</mat-label>
<mat-divider></mat-divider>
<mat-tab-group>
<mat-tab label="Markdown text | Result">
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
</mat-tab>
<mat-tab label="Editor">
<form novalidate>
<angular-markdown-editor style="color: #1a1a1a"
textareaId="editor2"
[options]="editorOptions"
name="markdownText"
[(ngModel)]="tutorial.body"
(onFullscreenExit)="hidePreview()"
>
</angular-markdown-editor>
</form>
</mat-tab> </mat-tab>
<mat-tab label="Result"> <mat-tab label="Result">
<markdown class="preview" [data]="tutorial.body"></markdown> <markdown class="preview" [data]="tutorial.body"></markdown>
@@ -9,9 +9,7 @@
.markdown-editor-container{ .markdown-editor-container{
display: flex; display: flex;
} }
mat-card{
margin: 20px;
}
mat-card-title{ mat-card-title{
color: cyan; color: cyan;
} }
@@ -55,3 +53,17 @@ mat-card-title{
/* display: block; /* display: block;
float: right;*/ float: right;*/
} }
.label-style{
font-size: -webkit-xxx-large;
font-weight: bold;
}
.mat-mdc-card-outlined {
background-color: var(--mat-card-outlined-container-color, var(--mat-sys-surface));
border-radius:0;
border-width: var(--mat-card-outlined-outline-width, 1px);
border-color: var(--mat-card-outlined-outline-color, var(--mat-sys-outline-variant));
box-shadow: var(--mat-card-outlined-container-elevation, var(--mat-sys-level0));
}
@@ -12,10 +12,11 @@ import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
import {MatTab, MatTabGroup} from '@angular/material/tabs'; import {MatTab, MatTabGroup} from '@angular/material/tabs';
import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {AngularMarkdownEditorModule, EditorInstance, EditorOption} from 'angular-markdown-editor'; import {AngularMarkdownEditorModule, EditorInstance, EditorOption} from 'angular-markdown-editor';
import {DialogLoginComponent} from '../../main-module/main.component/dialog-login.component/dialog-login.component';
import {GlobalConstants} from '../../../global-constants';
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import {DialogSelectImageComponent} from '../dialog-select-image.component/dialog-select-image.component'; import {DialogSelectImageComponent} from '../dialog-select-image.component/dialog-select-image.component';
import {DialogUploadImageComponent} from '../dialog-upload-image.component/dialog-upload-image.component';
import {FileUploadComponent} from '../../../components/file-upload.component/file-upload.component';
import {MatDivider} from '@angular/material/divider';
@Component({ @Component({
selector: 'app-tutorial-add.component', selector: 'app-tutorial-add.component',
@@ -33,7 +34,8 @@ import {DialogSelectImageComponent} from '../dialog-select-image.component/dialo
MatTabGroup, MatTabGroup,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
AngularMarkdownEditorModule AngularMarkdownEditorModule,
MatDivider
], ],
templateUrl: './tutorial-add.component.html', templateUrl: './tutorial-add.component.html',
styleUrl: './tutorial-add.component.scss', styleUrl: './tutorial-add.component.scss',
@@ -43,14 +45,16 @@ export class TutorialAddComponent implements OnInit{
tutorial: Tutorial = new Tutorial(); tutorial: Tutorial = new Tutorial();
submitted = false; submitted = false;
bsEditorInstance!: EditorInstance;
markdownText = ''; markdownText = '';
showEditor = true; showEditor = true;
bsEditorInstance!: EditorInstance;
tutorialForm!: FormGroup; tutorialForm!: FormGroup;
editorOptions!: EditorOption; editorOptions!: EditorOption;
readonly dialog = inject(MatDialog); readonly dialog = inject(MatDialog);
markdown = `## Markdown __rulez__! markdown = `## Markdown __rulez__!
--- ---
@@ -72,10 +76,6 @@ const language = 'typescript';
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private markdownService: MarkdownService, private markdownService: MarkdownService,
private userApiService: UserApiService, private userApiService: UserApiService,
private storageService: TokenStorageService,
private eventBusService: EventBusService,
private router: Router,
private authService: AuthService
) { ) {
this.tutorial.description = this.markdown; this.tutorial.description = this.markdown;
@@ -86,6 +86,7 @@ const language = 'typescript';
this.editorOptions = { this.editorOptions = {
autofocus: false, autofocus: false,
iconlibrary: 'fa', iconlibrary: 'fa',
height: 300,
savable: false, savable: false,
onFullscreenExit: (e) => this.hidePreview(), onFullscreenExit: (e) => this.hidePreview(),
onShow: (e) => this.bsEditorInstance = e, onShow: (e) => this.bsEditorInstance = e,
@@ -101,9 +102,7 @@ const language = 'typescript';
isPreview: [true] isPreview: [true]
}); });
} }
selectImage() {
throw new Error('Method not implemented.');
}
/** highlight all code found, needs to be wrapped in timer to work properly */ /** highlight all code found, needs to be wrapped in timer to work properly */
highlight() { highlight() {
setTimeout(() => { setTimeout(() => {
@@ -185,7 +184,7 @@ const language = 'typescript';
}); });
dialogSelectRef.componentInstance.uploadClicked.subscribe(result => { dialogSelectRef.componentInstance.uploadClicked.subscribe(result => {
dialogSelectRef.close(); dialogSelectRef.close();
//this.openSignupDialog(enterAnimationDuration, exitAnimationDuration); this.openUploadImageDialog(enterAnimationDuration, exitAnimationDuration);
}) })
const dialogSelectSubscription = dialogSelectRef.componentInstance.selectClicked const dialogSelectSubscription = dialogSelectRef.componentInstance.selectClicked
@@ -199,4 +198,29 @@ const language = 'typescript';
}); });
} }
openUploadImageDialog(enterAnimationDuration: string, exitAnimationDuration: string) {
let dialogSelectRef = this.dialog.open(DialogUploadImageComponent, {
height: '500px',
width: '600px',
enterAnimationDuration,
exitAnimationDuration,
// data: {username: this.dialogLoginData.username, password: this.dialogLoginData.password}
});
dialogSelectRef.componentInstance.openSelectDialogClicked.subscribe(result => {
dialogSelectRef.close();
this.openSelectImageDialog(enterAnimationDuration, exitAnimationDuration);
})
const dialogUploadSubscription = dialogSelectRef.componentInstance.selectUploadedImageClicked
.subscribe(result => {
console.log('Got the data!', result);
if (result == null) {
return;
}
this.tutorial.titleimage = result.url;
});
}
} }
@@ -4,23 +4,97 @@
</mat-card-header> </mat-card-header>
<mat-card-content > <mat-card-content >
<mat-form-field class="example-full-width"> <mat-form-field class="example-full-width">
<mat-label>Title</mat-label> <mat-label class="label-style">Title</mat-label>
<input matInput required <input matInput required
[(ngModel)]="tutorial.title"> [(ngModel)]="tutorial.title">
</mat-form-field> </mat-form-field>
<mat-tab-group>
<mat-tab label="Markdown text"> <mat-label class="label-style">Title image</mat-label>
<textarea class="variable-textarea" [(ngModel)]="tutorial.description"></textarea> <mat-divider></mat-divider>
<markdown class="variable-binding" [data]="tutorial.description"></markdown> <table class="example-full-width">
</mat-tab> <tr>
<mat-tab label="Result"> <td style="width: 120px;">
<markdown class="preview" [data]="tutorial.description"></markdown> <img style="width: 120px; height: 120px" src="{{tutorial.titleimage}}" alt="">
</mat-tab> </td>
<mat-tab label="Example"> <td>
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea> <mat-form-field class="example-full-width">
<markdown class="variable-binding" [data]="markdown"></markdown> <mat-label>Title image</mat-label>
</mat-tab> <input matInput disabled
</mat-tab-group> [(ngModel)]="tutorial.titleimage">
</mat-form-field>
<div>
<button mat-raised-button color="primary" (click)="openSelectImageDialog('100ms', '5ms')">Select image</button>
<button mat-raised-button color="primary" (click)="openUploadImageDialog('100ms', '5ms')">Upload image</button>
</div>
</td>
</tr>
</table>
<div class="example-full-width">
<mat-label class="label-style">Description</mat-label>
<mat-divider></mat-divider>
<mat-tab-group>
<mat-tab label="Markdown text | Result">
<textarea class="variable-textarea" [(ngModel)]="tutorial.description"></textarea>
<markdown class="variable-binding" [data]="tutorial.description"></markdown>
</mat-tab>
<mat-tab label="Editor">
<div class="markdown-editor-container">
<div class="markdown-editor">
<form novalidate>
<angular-markdown-editor
textareaId="editor1"
[options]="editorOptions"
name="markdownText"
[(ngModel)]="tutorial.description"
(onFullscreenExit)="hidePreview()"
>
</angular-markdown-editor>
</form>
</div>
</div>
</mat-tab>
<mat-tab label="Result">
<markdown class="preview" [data]="tutorial.description"></markdown>
</mat-tab>
<mat-tab label="Example">
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
<markdown class="variable-binding" [data]="markdown"></markdown>
</mat-tab>
</mat-tab-group>
</div>
<div class="example-full-width">
<mat-label class="label-style">Body</mat-label>
<mat-divider></mat-divider>
<mat-tab-group>
<mat-tab label="Markdown text | Result">
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
</mat-tab>
<mat-tab label="Editor">
<form novalidate>
<angular-markdown-editor style="color: #1a1a1a"
textareaId="editor2"
[options]="editorOptions"
name="markdownText"
[(ngModel)]="tutorial.body"
(onFullscreenExit)="hidePreview()"
>
</angular-markdown-editor>
</form>
</mat-tab>
<mat-tab label="Result">
<markdown class="preview" [data]="tutorial.body"></markdown>
</mat-tab>
<mat-tab label="Example">
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
<markdown class="variable-binding" [data]="markdown"></markdown>
</mat-tab>
</mat-tab-group>
</div>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
@@ -1,13 +1,18 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit} from '@angular/core';
import {MarkdownComponent} from 'ngx-markdown'; import {MarkdownComponent, MarkdownService} from 'ngx-markdown';
import {MatButton} from '@angular/material/button'; import {MatButton} from '@angular/material/button';
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from '@angular/material/card'; import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from '@angular/material/card';
import {MatError, MatFormField, MatInput, MatLabel} from '@angular/material/input'; import {MatError, MatFormField, MatInput, MatLabel} from '@angular/material/input';
import {MatTab, MatTabGroup} from '@angular/material/tabs'; import {MatTab, MatTabGroup} from '@angular/material/tabs';
import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Tutorial} from '../../../models/tutorial.model'; import {Tutorial} from '../../../models/tutorial.model';
import {UserApiService} from '../user-api.service'; import {UserApiService} from '../user-api.service';
import {ActivatedRoute, RouterLink} from '@angular/router'; import {ActivatedRoute, RouterLink} from '@angular/router';
import {AngularMarkdownEditorModule, EditorInstance, EditorOption} from 'angular-markdown-editor';
import {MatDivider} from '@angular/material/divider';
import {MatDialog} from '@angular/material/dialog';
import {DialogSelectImageComponent} from '../dialog-select-image.component/dialog-select-image.component';
import {DialogUploadImageComponent} from '../dialog-upload-image.component/dialog-upload-image.component';
@Component({ @Component({
selector: 'app-tutorial-edit.component', selector: 'app-tutorial-edit.component',
@@ -27,17 +32,28 @@ import {ActivatedRoute, RouterLink} from '@angular/router';
FormsModule, FormsModule,
MatFormField, MatFormField,
RouterLink, RouterLink,
MatError MatError,
AngularMarkdownEditorModule,
MatDivider
], ],
templateUrl: './tutorial-edit.component.html', templateUrl: './tutorial-edit.component.html',
styleUrl: './tutorial-edit.component.scss', styleUrl: './tutorial-edit.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })
export class TutorialEditComponent { export class TutorialEditComponent implements OnInit{
tutorial: Tutorial = new Tutorial(); tutorial: Tutorial = new Tutorial();
submitted = false; submitted = false;
hasError = false; hasError = false;
errorMessage = ''; errorMessage = '';
markdownText="";
bsEditorInstance!: EditorInstance;
tutorialForm!: FormGroup;
editorOptions!: EditorOption;
readonly dialog = inject(MatDialog);
markdown = `## Markdown __rulez__! markdown = `## Markdown __rulez__!
--- ---
@@ -56,7 +72,11 @@ const language = 'typescript';
> Blockquote to the max`; > Blockquote to the max`;
private id: string | null | undefined; private id: string | null | undefined;
constructor(private userApiService: UserApiService, private route: ActivatedRoute) { constructor(private userApiService: UserApiService,
private route: ActivatedRoute,
private fb: FormBuilder,
private markdownService: MarkdownService
) {
this.route.queryParams this.route.queryParams
.subscribe(params => { .subscribe(params => {
@@ -71,6 +91,62 @@ const language = 'typescript';
} }
); );
} }
ngOnInit(): void {
this.editorOptions = {
autofocus: false,
iconlibrary: 'fa',
height: 300,
savable: false,
onFullscreenExit: (e) => this.hidePreview(),
onShow: (e) => this.bsEditorInstance = e,
parser: (val) => this.parse(val)
};
this.buildForm(this.tutorial.description);
}
buildForm(markdownText: string | undefined) {
this.tutorialForm = this.fb.group({
body: [markdownText],
isPreview: [true]
});
}
/** highlight all code found, needs to be wrapped in timer to work properly */
highlight() {
setTimeout(() => {
this.markdownService.highlight();
});
}
hidePreview() {
if (this.bsEditorInstance && this.bsEditorInstance.hidePreview) {
this.bsEditorInstance.hidePreview();
}
}
showFullScreen(isFullScreen: boolean) {
if (this.bsEditorInstance && this.bsEditorInstance.setFullscreen) {
this.bsEditorInstance.showPreview();
this.bsEditorInstance.setFullscreen(isFullScreen);
}
}
parse(inputValue: string) {
const markedOutput = this.markdownService.parse(inputValue.trim());
this.highlight();
return markedOutput;
}
onFormChanges(): void {
this.tutorialForm.valueChanges.subscribe(formData => {
if (formData) {
this.markdownText = formData.body;
}
});
}
updateTutorial(): void { updateTutorial(): void {
this.userApiService.update(this.id, this.tutorial) this.userApiService.update(this.id, this.tutorial)
.subscribe( .subscribe(
@@ -87,4 +163,55 @@ const language = 'typescript';
this.hasError = true; this.hasError = true;
}); });
} }
openSelectImageDialog(enterAnimationDuration: string, exitAnimationDuration: string) {
let dialogSelectRef = this.dialog.open(DialogSelectImageComponent, {
height: '500px',
width: '600px',
enterAnimationDuration,
exitAnimationDuration,
// data: {username: this.dialogLoginData.username, password: this.dialogLoginData.password}
});
dialogSelectRef.componentInstance.uploadClicked.subscribe(result => {
dialogSelectRef.close();
this.openUploadImageDialog(enterAnimationDuration, exitAnimationDuration);
})
const dialogSelectSubscription = dialogSelectRef.componentInstance.selectClicked
.subscribe(result => {
console.log('Got the data!', result);
if (result == null) {
return;
}
this.tutorial.titleimage = result.url;
});
}
openUploadImageDialog(enterAnimationDuration: string, exitAnimationDuration: string) {
let dialogSelectRef = this.dialog.open(DialogUploadImageComponent, {
height: '500px',
width: '600px',
enterAnimationDuration,
exitAnimationDuration,
// data: {username: this.dialogLoginData.username, password: this.dialogLoginData.password}
});
dialogSelectRef.componentInstance.openSelectDialogClicked.subscribe(result => {
dialogSelectRef.close();
this.openSelectImageDialog(enterAnimationDuration, exitAnimationDuration);
})
const dialogUploadSubscription = dialogSelectRef.componentInstance.selectUploadedImageClicked
.subscribe(result => {
console.log('Got the data!', result);
if (result == null) {
return;
}
this.tutorial.titleimage = result.url;
});
}
} }
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {forkJoin, Observable} from 'rxjs'; import {forkJoin, Observable} from 'rxjs';
import {Tutorial} from '../../models/tutorial.model'; import {Tutorial} from '../../models/tutorial.model';
import {HttpClient, HttpHeaders} from '@angular/common/http'; import {HttpClient, HttpEvent, HttpHeaders, HttpRequest} from '@angular/common/http';
import {GlobalConstants} from '../../global-constants'; import {GlobalConstants} from '../../global-constants';
import {FileInfo} from '../../models/file-info'; import {FileInfo} from '../../models/file-info';
@@ -15,6 +15,19 @@ export class UserApiService {
} }
upload(file: File): Observable<HttpEvent<any>> {
const formData: FormData = new FormData();
formData.append('file', file);
const req = new HttpRequest('POST', `${this.baseUrl}/upload`, formData, {
reportProgress: true,
responseType: 'json'
});
return this.http.request(req);
}
saveZhipuaiImage(url: string): Observable<any> { saveZhipuaiImage(url: string): Observable<any> {
return this.http.post(`${this.baseUrl}/saveZhipuAiImage`, {imageUrl:url}); return this.http.post(`${this.baseUrl}/saveZhipuAiImage`, {imageUrl:url});
} }
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import {userRouting} from './user.routing'; import {userRouting} from './user.routing';
import {UserComponent} from './user.component/user.component'; import {UserComponent} from './user.component/user.component';
import {AngularMarkdownEditorModule} from 'angular-markdown-editor'; import {AngularMarkdownEditorModule} from 'angular-markdown-editor';
import {MatFormFieldModule} from '@angular/material/form-field';
@@ -12,7 +13,8 @@ import {AngularMarkdownEditorModule} from 'angular-markdown-editor';
userRouting, userRouting,
UserComponent, UserComponent,
CommonModule, CommonModule,
AngularMarkdownEditorModule AngularMarkdownEditorModule,
MatFormFieldModule
] ]
}) })
export class UserModule { } export class UserModule { }
@@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { FileUploadApiService } from './file-upload-api.service';
describe('FileUploadApiService', () => {
let service: FileUploadApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(FileUploadApiService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -1,29 +0,0 @@
import { Injectable } from '@angular/core';
import {GlobalConstants} from '../global-constants';
import {HttpClient, HttpEvent, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';
import {FileInfo} from '../models/file-info';
@Injectable({
providedIn: 'root'
})
export class FileUploadService {
private baseUrl = GlobalConstants.API_URL + '/file';
constructor(private http: HttpClient) {}
upload(file: File): Observable<HttpEvent<any>> {
const formData: FormData = new FormData();
formData.append('file', file);
const req = new HttpRequest('POST', `${this.baseUrl}/upload`, formData, {
responseType: 'json',
});
return this.http.request(req);
}
getFiles(): Observable<FileInfo[]> {
return this.http.get<FileInfo[]>(`${this.baseUrl}/files`);
}
}
@@ -7,6 +7,7 @@
FROM openjdk:24 FROM openjdk:24
WORKDIR /jambotron/ WORKDIR /jambotron/
VOLUME /jambotron_data/uploads
COPY './build/libs/jambotron-0.0.1-SNAPSHOT.jar' '/app/jambotron.jar' COPY './build/libs/jambotron-0.0.1-SNAPSHOT.jar' '/app/jambotron.jar'
#COPY --from=BUILD_IMAGE /jambotron/build/libs/jambotron-0.0.1-SNAPSHOT.jar . #COPY --from=BUILD_IMAGE /jambotron/build/libs/jambotron-0.0.1-SNAPSHOT.jar .
#EXPOSE 8080 #EXPOSE 8080
@@ -4,7 +4,7 @@ services:
container_name: 'jambotron-container' container_name: 'jambotron-container'
build: build:
context: ../../ context: ../../
dockerfile: ./src/Docker/Dockerfile dockerfile: /Dockerfile
ports: ports:
# - "8081:80" # - "8081:80"
- "8443:443" - "8443:443"
@@ -12,6 +12,7 @@ services:
- postgres_jambotron - postgres_jambotron
volumes: volumes:
- certs:/certs - certs:/certs
- jambotron_data:/jambotron_data
# env_file: "webapp.env" # env_file: "webapp.env"
environment: environment:
SSL_ENABLED: "true" SSL_ENABLED: "true"
@@ -64,3 +65,5 @@ services:
volumes: volumes:
certs: certs:
external: true external: true
jambotron_data:
# external: true
+5
View File
@@ -0,0 +1,5 @@
// investigate docker-compose.yml files for details on how to run the application
@echo off
//docker compose -f docker-compose.yml down
//docker compose -f docker-compose.yml build
//docker compose -f docker-compose.yml up -d
+9
View File
@@ -0,0 +1,9 @@
#additional investigation needed to run this script
##!/bin/bash
#
## stop any previously running containers
#docker compose --env-file .env -f DevOps/docker-compose.yml down
## build the images
#docker compose --env-file .env -f DevOps/docker-compose.yml build
## start the containers
#docker compose --env-file .env -f DevOps/docker-compose.yml up -d
@@ -5,15 +5,9 @@ import com.jambotronGroup.jambotron.fileUpload.FilesStorageService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.event.Level;
import org.slf4j.helpers.BasicMarker;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
import java.util.Iterator;
@SpringBootApplication @SpringBootApplication
public class JambotronApplication implements CommandLineRunner { public class JambotronApplication implements CommandLineRunner {
@@ -29,10 +23,12 @@ public class JambotronApplication implements CommandLineRunner {
logger.error("Application Run. This is an error message."); logger.error("Application Run. This is an error message.");
logger.debug("Application Run. This is a debug message."); logger.debug("Application Run. This is a debug message.");
logger.info("Application Run. This is an info message."); logger.info("Application Run. This is an info message.");
logger.info("Application Run. This is an info message.!!!!!!");
} }
@Override @Override
public void run(String... arg) throws Exception { public void run(String... arg) throws Exception {
// storageService.deleteAll(); // storageService.deleteAll();
storageService.init(); storageService.init();
} }
} }
@@ -1,25 +0,0 @@
package com.jambotronGroup.jambotron.configuretions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/*
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Value("${spring.resources.static-locations}")
String resourceLocations;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//registry.addResourceHandler("/public/**").addResourceLocations(resourceLocations);
registry.addResourceHandler("/**").addResourceLocations("classpath:/resources/");
//registry.
registry.addResourceHandler("/media/**").addResourceLocations("resources/main/public/media/");
}
}*/
@@ -1,35 +0,0 @@
package com.jambotronGroup.jambotron.configuretions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
/*
@ControllerAdvice
public class NotFoundHandler {
@Value("${spa.default-file}")
String defaultFile;
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<String> renderDefaultPage() {
try {
File indexFile = ResourceUtils.getFile(defaultFile);
FileInputStream inputStream = new FileInputStream(indexFile);
String body = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(body);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("There was an error completing the action.");
}
}
}*/
@@ -1,12 +1,14 @@
package com.jambotronGroup.jambotron.controllers; package com.jambotronGroup.jambotron.controllers;
import com.jambotronGroup.jambotron.fileUpload.FilesStorageService; import com.jambotronGroup.jambotron.fileUpload.FilesStorageService;
import com.jambotronGroup.jambotron.fileUpload.ResponseImageUploadResult;
import com.jambotronGroup.jambotron.fileUpload.ResponseMessage; import com.jambotronGroup.jambotron.fileUpload.ResponseMessage;
import com.jambotronGroup.jambotron.model.FileInfo; import com.jambotronGroup.jambotron.model.FileInfo;
import com.jambotronGroup.jambotron.model.User; import com.jambotronGroup.jambotron.model.User;
import com.jambotronGroup.jambotron.payload.request.SaveZhipuAiImageRequest; import com.jambotronGroup.jambotron.payload.request.SaveZhipuAiImageRequest;
import com.jambotronGroup.jambotron.security.AuthenticationFacade; import com.jambotronGroup.jambotron.security.AuthenticationFacade;
import com.jambotronGroup.jambotron.system.SystemServiceImpl; import com.jambotronGroup.jambotron.system.SystemServiceImpl;
import com.jambotronGroup.jambotron.utils.FilesRoutingHelper;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -30,17 +32,16 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Controller @Controller
//@CrossOrigin("http://localhost:8081")
public class FilesController { public class FilesController {
private static final Logger logger = LoggerFactory.getLogger(FilesController.class); private static final Logger logger = LoggerFactory.getLogger(FilesController.class);
@Autowired @Autowired
AuthenticationFacade authenticationFacade; AuthenticationFacade authenticationFacade;
@Autowired @Autowired
FilesStorageService storageService; FilesStorageService storageService;
@PostMapping("/api/user/saveZhipuAiImage") @PostMapping("/api/user/saveZhipuAiImage")
public ResponseEntity<ResponseMessage> saveZhipuAiImage(@Valid @RequestBody SaveZhipuAiImageRequest request) { public ResponseEntity<ResponseMessage> saveZhipuAiImage(@Valid @RequestBody SaveZhipuAiImageRequest request) {
@@ -52,7 +53,6 @@ public class FilesController {
String message = ""; String message = "";
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
try { try {
@@ -92,14 +92,21 @@ public class FilesController {
return ResponseEntity.status(HttpStatus.OK).body(new ResponseMessage(message)); return ResponseEntity.status(HttpStatus.OK).body(new ResponseMessage(message));
} }
@PostMapping("/api/file/upload") @PostMapping("/api/user/upload")
public ResponseEntity<ResponseMessage> uploadFile(@RequestParam("file") MultipartFile file) { public ResponseEntity<ResponseMessage> uploadFile(@RequestParam("file") MultipartFile file) {
String message = ""; String message = "";
try { try {
storageService.save(file); User user = authenticationFacade.getUser();
String localFullFileName = storageService.save(user.getId().toString(), file);
FileInfo fileInfo = new FileInfo(
file.getOriginalFilename(),
FilesRoutingHelper.getUserImageUrl(localFullFileName));
message = "Uploaded the file successfully: " + file.getOriginalFilename(); message = "Uploaded the file successfully: " + file.getOriginalFilename();
return ResponseEntity.status(HttpStatus.OK).body(new ResponseMessage(message)); return ResponseEntity.status(HttpStatus.OK).body(new ResponseImageUploadResult(message,fileInfo));
} catch (Exception e) { } catch (Exception e) {
message = "Could not upload the file: " + file.getOriginalFilename() + ". Error: " + e.getMessage(); message = "Could not upload the file: " + file.getOriginalFilename() + ". Error: " + e.getMessage();
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new ResponseMessage(message)); return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new ResponseMessage(message));
@@ -113,8 +120,8 @@ public class FilesController {
List<FileInfo> fileInfos = storageService.loadUserImages(user.getId().toString()).map(path -> { List<FileInfo> fileInfos = storageService.loadUserImages(user.getId().toString()).map(path -> {
String filename = path.getFileName().toString(); String filename = path.getFileName().toString();
String url = MvcUriComponentsBuilder
.fromMethodName(FilesController.class, "getUserImage", path.getFileName().toString()).build().toString(); String url = FilesRoutingHelper.getUserImageUrl(filename);
return new FileInfo(filename, url); return new FileInfo(filename, url);
}).collect(Collectors.toList()); }).collect(Collectors.toList());
@@ -127,9 +134,7 @@ public class FilesController {
public ResponseEntity<List<FileInfo>> getListFiles() { public ResponseEntity<List<FileInfo>> getListFiles() {
List<FileInfo> fileInfos = storageService.loadAll().map(path -> { List<FileInfo> fileInfos = storageService.loadAll().map(path -> {
String filename = path.getFileName().toString(); String filename = path.getFileName().toString();
String url = MvcUriComponentsBuilder String url = FilesRoutingHelper.getPublicImageUrl(filename);
.fromMethodName(FilesController.class, "getFile", path.getFileName().toString()).build().toString();
return new FileInfo(filename, url); return new FileInfo(filename, url);
}).collect(Collectors.toList()); }).collect(Collectors.toList());
@@ -9,6 +9,7 @@ import com.jambotronGroup.jambotron.repository.TutorialRepository;
import com.jambotronGroup.jambotron.repository.UserRepository; import com.jambotronGroup.jambotron.repository.UserRepository;
import com.jambotronGroup.jambotron.security.AuthenticationFacade; import com.jambotronGroup.jambotron.security.AuthenticationFacade;
import com.jambotronGroup.jambotron.security.services.UserDetailsImpl; import com.jambotronGroup.jambotron.security.services.UserDetailsImpl;
import com.jambotronGroup.jambotron.utils.FilesRoutingHelper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -118,8 +119,7 @@ public class TutorialController {
String.format("Tutorial_%s",newFilename ) String.format("Tutorial_%s",newFilename )
); );
String url = MvcUriComponentsBuilder String url = FilesRoutingHelper.getPublicImageUrl(path.getFileName().toString());
.fromMethodName(FilesController.class, "getFile", path.getFileName().toString()).build().toString();
try { try {
@@ -160,7 +160,12 @@ public class TutorialController {
} }
@PutMapping("user/tutorial-update/{id}") @PutMapping("user/tutorial-update/{id}")
public ResponseEntity<?> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) { public ResponseEntity<?> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial){
User user = authenticationFacade.getUser();
Optional<Tutorial> tutorialData = tutorialRepository.findById(id); Optional<Tutorial> tutorialData = tutorialRepository.findById(id);
Map<String, Object> map = new LinkedHashMap<String, Object>(); Map<String, Object> map = new LinkedHashMap<String, Object>();
if (tutorialData.isPresent()) { if (tutorialData.isPresent()) {
@@ -169,7 +174,24 @@ public class TutorialController {
servTutorial.setDescription(tutorial.getDescription()); servTutorial.setDescription(tutorial.getDescription());
servTutorial.setPublished(tutorial.isPublished()); servTutorial.setPublished(tutorial.isPublished());
servTutorial.setTobepublished(tutorial.isTobepublished()); servTutorial.setTobepublished(tutorial.isTobepublished());
try { try {
String imageFileName = FilesStorageServiceImpl.getFileNameFromUrl(tutorial.getTitleimage());
String servImageFileName = FilesStorageServiceImpl.getFileNameFromUrl(servTutorial.getTitleimage());
if(!servImageFileName.equals(imageFileName)){
filesStorageService.deletePublicFile(servImageFileName);
Path path= filesStorageService.moveFile(
user.getId().toString(),
tutorial.getTitleimage(),
String.format("Tutorial_%s",imageFileName )
);
servTutorial.setTitleimage(FilesRoutingHelper.getPublicImageUrl(path.getFileName().toString()));
}
servTutorial = tutorialRepository.save(servTutorial); servTutorial = tutorialRepository.save(servTutorial);
} catch (Exception e) { } catch (Exception e) {
@@ -0,0 +1,18 @@
package com.jambotronGroup.jambotron.exceptionHandlers;
import com.jambotronGroup.jambotron.fileUpload.ResponseMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
//@ControllerAdvice
//public class FileUploadExceptionHandler extends ResponseEntityExceptionHandler {
//
// @ExceptionHandler(MaxUploadSizeExceededException.class)
// public ResponseEntity<ResponseMessage> handleMaxSizeException(MaxUploadSizeExceededException exc) {
// return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new ResponseMessage("File too large!"));
// }
//}
@@ -10,7 +10,7 @@ import java.util.stream.Stream;
public interface FilesStorageService { public interface FilesStorageService {
public void init(); public void init();
public void save(String userID,MultipartFile file); public String save(String userID,MultipartFile file);
public void save(MultipartFile file); public void save(MultipartFile file);
@@ -25,4 +25,6 @@ public interface FilesStorageService {
public Stream<Path> loadUserImages(String userID); public Stream<Path> loadUserImages(String userID);
public Path moveFile(String userID, String url, String newFilename) throws Exception; public Path moveFile(String userID, String url, String newFilename) throws Exception;
public void deletePublicFile(String filename);
} }
@@ -19,9 +19,14 @@ import java.util.stream.Stream;
@Service @Service
public class FilesStorageServiceImpl implements FilesStorageService { public class FilesStorageServiceImpl implements FilesStorageService {
private final Path root = Paths.get("uploads/user-images/"); // private final Path root = Paths.get("uploads/user-images/");
//
// private final Path rootPublic = Paths.get("uploads/public-images/");
// Update paths to use the Docker volume
private final Path root = Paths.get("/jambotron_data/uploads/user-images/");
private final Path rootPublic = Paths.get("/jambotron_data/uploads/public-images/");
private final Path rootPublic = Paths.get("uploads/public-images/");
@Override @Override
public void init() { public void init() {
@@ -39,6 +44,16 @@ public class FilesStorageServiceImpl implements FilesStorageService {
return path.substring(path.lastIndexOf('/') + 1); // Extract the file name return path.substring(path.lastIndexOf('/') + 1); // Extract the file name
} }
/**
* Moves a file from a user's directory to the public directory with a new name.
* StandardCopyOption.REPLACE_EXISTING
*
* @param userID The ID of the user owning the file.
* @param url The URL of the file to move.
* @param newFilename The new name for the file in the public directory.
* @return The path to the moved file in the public directory.
* @throws Exception If the file cannot be moved.
*/
@Override @Override
public Path moveFile(String userID, String url, String newFilename) throws Exception { public Path moveFile(String userID, String url, String newFilename) throws Exception {
@@ -57,13 +72,34 @@ public class FilesStorageServiceImpl implements FilesStorageService {
} }
@Override @Override
public void save(String userID,MultipartFile file) { public void deletePublicFile(String filename) {
try {
Path filePath = this.rootPublic.resolve(filename);
Files.deleteIfExists(filePath);
} catch (IOException e) {
throw new RuntimeException("Could not delete the file: " + e.getMessage());
}
}
/**
* Saves a file to a user's directory.
* uploads/user-images/{userID}/{filename}
*
* @param userID The ID of the user.
* @param file The file to save.
*/
@Override
public String save(String userID,MultipartFile file) {
Path targetPath = null;
try { try {
Path path = this.root.resolve(userID); Path path = this.root.resolve(userID);
path.toFile().mkdirs(); // Ensure user directory exists path.toFile().mkdirs(); // Ensure user directory exists
Files.copy(file.getInputStream(), path.resolve(file.getOriginalFilename()), targetPath = path.resolve(file.getOriginalFilename());
Files.copy(file.getInputStream(), targetPath,
java.nio.file.StandardCopyOption.REPLACE_EXISTING); java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof FileAlreadyExistsException) { if (e instanceof FileAlreadyExistsException) {
throw new RuntimeException("A file of that name already exists."); throw new RuntimeException("A file of that name already exists.");
@@ -71,6 +107,8 @@ public class FilesStorageServiceImpl implements FilesStorageService {
throw new RuntimeException(e.getMessage()); throw new RuntimeException(e.getMessage());
} }
return targetPath.getFileName().toString();
} }
@Override @Override
@@ -0,0 +1,21 @@
package com.jambotronGroup.jambotron.fileUpload;
import com.jambotronGroup.jambotron.model.FileInfo;
public class ResponseImageUploadResult extends ResponseMessage {
private FileInfo fileInfo;
public ResponseImageUploadResult(String message, FileInfo fileInfo) {
super(message);
this.fileInfo = fileInfo;
}
public FileInfo getFileInfo() {
return this.fileInfo;
}
public void setFileInfo(FileInfo fileInfo) {
this.fileInfo = fileInfo;
}
}
@@ -3,6 +3,8 @@ package com.jambotronGroup.jambotron.fileUpload;
public class ResponseMessage { public class ResponseMessage {
private String message; private String message;
public ResponseMessage(String message) { public ResponseMessage(String message) {
this.message = message; this.message = message;
} }
@@ -27,7 +27,7 @@ public class Tutorial {
@Column(name = "tobepublished") @Column(name = "tobepublished")
private boolean tobepublished; private boolean tobepublished;
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
@JoinColumn(name = "userID", nullable = false) @JoinColumn(name = "userID", nullable = false)
private User user; private User user;
@@ -104,6 +104,9 @@ public class WebSecurityConfig implements WebMvcConfigurer {// extends WebSecuri
.requestMatchers("/tutorials-images/**").permitAll() .requestMatchers("/tutorials-images/**").permitAll()
.requestMatchers("/api/file/files").permitAll()
.requestMatchers("/api/file/upload").permitAll()
.requestMatchers("/files/**").permitAll() .requestMatchers("/files/**").permitAll()
.requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/auth/**").permitAll()
@@ -0,0 +1,25 @@
package com.jambotronGroup.jambotron.utils;
import com.jambotronGroup.jambotron.controllers.FilesController;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
@Component
public class FilesRoutingHelper {
/**
* Generates a URL for accessing a user's image.
* format: /api/user/user-images/{filename:.+}
*
* @param filename The name of the file.
* @return The URL to access the file.
*/
public static String getUserImageUrl(String filename) {
return MvcUriComponentsBuilder
.fromMethodName(FilesController.class, "getUserImage", filename).build().toString();
}
public static String getPublicImageUrl(String filename) {
return MvcUriComponentsBuilder
.fromMethodName(FilesController.class, "getFile", filename).build().toString();
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
#bootJar with profile prod #bootJar with profile prod
spring.profiles.active=prod #spring.profiles.active=prod
#spring.profiles.active=dev spring.profiles.active=dev