Refactor image URL handling and enhance tutorial component layout

This commit is contained in:
liosha84
2025-08-06 03:58:16 +03:00
parent d2f46f4405
commit d6eed2e3e1
11 changed files with 380 additions and 97 deletions
@@ -34,7 +34,7 @@ export class DialogUploadImageComponent {
} }
selectUploadedImage() { selectUploadedImage() {
this.selectUploadedImageClicked.emit(this.imageUpload?.fileInfo?.url); this.selectUploadedImageClicked.emit(this.imageUpload?.fileInfo);
this.dialogRef.close(); this.dialogRef.close();
} }
@@ -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">
</mat-form-field>
<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 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));
}
@@ -16,6 +16,7 @@ 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 {DialogUploadImageComponent} from '../dialog-upload-image.component/dialog-upload-image.component';
import {FileUploadComponent} from '../../../components/file-upload.component/file-upload.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 {FileUploadComponent} from '../../../components/file-upload.component/fil
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,9 +45,9 @@ 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;
@@ -84,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,
@@ -99,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(() => {
@@ -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;
});
}
} }
@@ -8,6 +8,7 @@ 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;
@@ -40,22 +41,6 @@ public class FilesController {
@Autowired @Autowired
FilesStorageService storageService; FilesStorageService storageService;
/**
* 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.
*/
private static String getUserImageUrl(String filename) {
return MvcUriComponentsBuilder
.fromMethodName(FilesController.class, "getUserImage", filename).build().toString();
}
private static String getPublicImageUrl(String filename) {
return MvcUriComponentsBuilder
.fromMethodName(FilesController.class, "getFile", filename).build().toString();
}
@PostMapping("/api/user/saveZhipuAiImage") @PostMapping("/api/user/saveZhipuAiImage")
public ResponseEntity<ResponseMessage> saveZhipuAiImage(@Valid @RequestBody SaveZhipuAiImageRequest request) { public ResponseEntity<ResponseMessage> saveZhipuAiImage(@Valid @RequestBody SaveZhipuAiImageRequest request) {
@@ -117,8 +102,8 @@ public class FilesController {
FileInfo fileInfo = new FileInfo( FileInfo fileInfo = new FileInfo(
file.getOriginalFilename(), file.getOriginalFilename(),
FilesController.getUserImageUrl(localFullFileName)); FilesRoutingHelper.getUserImageUrl(localFullFileName));
message = "Uploaded the file successfully: " + file.getOriginalFilename(); message = "Uploaded the file successfully: " + file.getOriginalFilename();
return ResponseEntity.status(HttpStatus.OK).body(new ResponseImageUploadResult(message,fileInfo)); return ResponseEntity.status(HttpStatus.OK).body(new ResponseImageUploadResult(message,fileInfo));
@@ -136,7 +121,7 @@ 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 = FilesController.getUserImageUrl(filename); String url = FilesRoutingHelper.getUserImageUrl(filename);
return new FileInfo(filename, url); return new FileInfo(filename, url);
}).collect(Collectors.toList()); }).collect(Collectors.toList());
@@ -149,7 +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 = FilesController.getPublicImageUrl(filename); String url = FilesRoutingHelper.getPublicImageUrl(filename);
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,22 @@ 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());
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) {
@@ -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);
} }
@@ -39,6 +39,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 {
@@ -56,6 +66,16 @@ public class FilesStorageServiceImpl implements FilesStorageService {
return this.rootPublic.resolve(newFilename); return this.rootPublic.resolve(newFilename);
} }
@Override
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. * Saves a file to a user's directory.
* uploads/user-images/{userID}/{filename} * uploads/user-images/{userID}/{filename}
@@ -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();
}
}