Compare commits
12
Commits
textures
...
213f9987d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
213f9987d9 | ||
|
|
48cac607de | ||
|
|
6ed2e15017 | ||
|
|
7fc25f46f3 | ||
|
|
c72703ea32 | ||
|
|
e837a9216d | ||
|
|
d4363b3a8a | ||
|
|
1e5862bae6 | ||
|
|
daf88ea437 | ||
|
|
9abd570b87 | ||
|
|
5284d498f3 | ||
|
|
c3a7be82e9 |
@@ -36,10 +36,10 @@ public class SecurityConfig {
|
|||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
return http
|
return http
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll()
|
|
||||||
.requestMatchers("/team/**", "/history/**").permitAll()
|
|
||||||
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||||
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
|
// .requestMatchers("/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
|
.requestMatchers("/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
)
|
)
|
||||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ public class LoginController implements LoginApi {
|
|||||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<String> login(String code) {
|
public ResponseEntity<String> login(String code) {
|
||||||
CacheEntry cacheEntry1 = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
|
||||||
cache.put("23232323", cacheEntry1);
|
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
@@ -134,12 +132,12 @@ public class LoginController implements LoginApi {
|
|||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
|
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
|
||||||
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
|
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
|
||||||
CompletableFuture<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>();
|
CompletableFuture<Optional<PrivilegedUser>> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||||
List<PermissionClaimDto> claimList = new ArrayList<>();
|
List<PermissionClaimDto> claimList = new ArrayList<>();
|
||||||
Connection.getConnection(Databases.DEFAULT)
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
.runQuery(sqlSession -> {
|
.runQuery(sqlSession -> {
|
||||||
try {
|
try {
|
||||||
PrivilegedUser privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
|
Optional<PrivilegedUser> privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
|
||||||
.getUserByUuid(uuid.toString());
|
.getUserByUuid(uuid.toString());
|
||||||
|
|
||||||
privilegedUserCompletableFuture.complete(privilegedUser);
|
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||||
@@ -148,17 +146,15 @@ public class LoginController implements LoginApi {
|
|||||||
privilegedUserCompletableFuture.completeExceptionally(e);
|
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join();
|
Optional<PrivilegedUser> privilegedUser = privilegedUserCompletableFuture.join();
|
||||||
claimList.add(PermissionClaimDto.USER);
|
claimList.add(PermissionClaimDto.USER);
|
||||||
if (privilegedUser != null) {
|
privilegedUser.ifPresent(user -> user.getPermissions().forEach(permission -> {
|
||||||
privilegedUser.getPermissions().forEach(permission -> {
|
|
||||||
try {
|
try {
|
||||||
claimList.add(PermissionClaimDto.valueOf(permission));
|
claimList.add(PermissionClaimDto.valueOf(permission));
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.warn("Received invalid permission claim: {}", permission);
|
log.warn("Received invalid permission claim: {}", permission);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||||
.issuer("altitudeweb")
|
.issuer("altitudeweb")
|
||||||
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||||
|
|||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.particles;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.ParticlesApi;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
public class ParticleController implements ParticlesApi {
|
||||||
|
|
||||||
|
@Value("${login.secret:#{null}}")
|
||||||
|
private String loginSecret;
|
||||||
|
|
||||||
|
@Value("${particles.file_path}")
|
||||||
|
private String particlesFilePath;
|
||||||
|
|
||||||
|
@Value("${notification.server.url:http://localhost:8080}")
|
||||||
|
private String notificationServerUrl;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Resource> downloadFile(String authorization, String filename) throws Exception {
|
||||||
|
if (authorization == null || !authorization.equals(loginSecret)) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
File file = new File(particlesFilePath);
|
||||||
|
if (!file.exists() || !file.isDirectory()) {
|
||||||
|
log.error("Particles file path {} is not a directory, not downloading particles file", particlesFilePath);
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
File targetFile = new File(file, filename);
|
||||||
|
return getFileForDownload(targetFile, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Resource> downloadFileForUser(String authorization, String uuid, String filename) throws Exception {
|
||||||
|
if (authorization == null || !authorization.equals(loginSecret)) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
File file = new File(particlesFilePath);
|
||||||
|
if (!file.exists() || !file.isDirectory()) {
|
||||||
|
log.error("Particles file path {} is not a directory, not downloading particles user file", particlesFilePath);
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
File targetDir = new File(file, uuid);
|
||||||
|
if (targetDir.exists()) {
|
||||||
|
return getFileForDownload(targetDir, filename);
|
||||||
|
} else {
|
||||||
|
log.warn("User {} does not have a directory for particles files", uuid);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Resource> getFileForDownload(File file, String filename) {
|
||||||
|
File targetFile = new File(file, filename);
|
||||||
|
if (!targetFile.exists()) {
|
||||||
|
log.warn("Particles file {} does not exist", targetFile.getAbsolutePath());
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetFile.isFile()) {
|
||||||
|
log.warn("Particles file {} is not a file", targetFile.getAbsolutePath());
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path path = targetFile.toPath();
|
||||||
|
ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentLength(targetFile.length())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||||
|
.body(resource);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to read particles file {}: {}", targetFile.getAbsolutePath(), e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> saveFile(String filename, MultipartFile content) throws Exception {
|
||||||
|
File file = new File(particlesFilePath);
|
||||||
|
if (!file.exists() || !file.isDirectory()) {
|
||||||
|
log.error("Particles file path {} is not a directory, not saving particles file", particlesFilePath);
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
|
||||||
|
notifyServerOfFileUpload(filename);
|
||||||
|
return voidResponseEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> saveFileForUser(String uuid, String filename, MultipartFile content) throws Exception {
|
||||||
|
File file = new File(particlesFilePath);
|
||||||
|
if (!file.exists() || !file.isDirectory()) {
|
||||||
|
log.error("Particles file path {} is not a directory, not saving particles user file", particlesFilePath);
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
File targetDir = new File(file, uuid);
|
||||||
|
if (!file.exists()) {
|
||||||
|
log.debug("Creating particles directory {}", targetDir.getAbsolutePath());
|
||||||
|
if (targetDir.mkdirs()) {
|
||||||
|
log.info("Created particles user directory {}", targetDir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
|
||||||
|
notifyServerOfFileUpload(uuid, filename);
|
||||||
|
return voidResponseEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyServerOfFileUpload(String filename) {
|
||||||
|
String notificationUrl = String.format("%s/notify/%s.json", notificationServerUrl, filename);
|
||||||
|
sendNotification(notificationUrl, String.format("file upload: %s", filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyServerOfFileUpload(String uuid, String filename) {
|
||||||
|
String notificationUrl = String.format("%s/notify/%s/%s.json", notificationServerUrl, uuid, filename);
|
||||||
|
sendNotification(notificationUrl, String.format("file upload for user %s: %s", uuid, filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendNotification(String notificationUrl, String logDescription) {
|
||||||
|
try {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
ResponseEntity<String> response = restTemplate.getForEntity(notificationUrl, String.class);
|
||||||
|
|
||||||
|
if (response.getStatusCode().is2xxSuccessful()) {
|
||||||
|
log.info("Successfully notified server of {}", logDescription);
|
||||||
|
} else {
|
||||||
|
log.warn("Failed to notify server of {}, status: {}",
|
||||||
|
logDescription, response.getStatusCode());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error notifying server of {}", logDescription, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ResponseEntity<Void> writeContentToFile(File dir, String filename, MultipartFile content) {
|
||||||
|
File targetFile = new File(dir, filename);
|
||||||
|
if (!Files.isWritable(targetFile.toPath())) {
|
||||||
|
log.error("Particles file {} is not writable", targetFile.getAbsolutePath());
|
||||||
|
return ResponseEntity.status(403).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetFile.exists()) {
|
||||||
|
log.warn("Overwriting existing particles file {}", targetFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
content.transferTo(targetFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to write particles file {}", targetFile.getAbsolutePath(), e);
|
||||||
|
return ResponseEntity.status(500).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,6 @@ database.user=${DB_USER:root}
|
|||||||
database.password=${DB_PASSWORD:root}
|
database.password=${DB_PASSWORD:root}
|
||||||
cors.allowed-origins=${CORS:https://alttd.com}
|
cors.allowed-origins=${CORS:https://alttd.com}
|
||||||
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||||
|
particles.file_path=${user.home}/.altitudeweb/particles
|
||||||
|
notification.server.url=${SERVER_IP:10.0.0.107}:${SERVER_PORT:8080}
|
||||||
logging.level.com.alttd.altitudeweb=INFO
|
logging.level.com.alttd.altitudeweb=INFO
|
||||||
|
|||||||
+3
-1
@@ -1,8 +1,10 @@
|
|||||||
package com.alttd.altitudeweb.database.web_db;
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.*;
|
import org.apache.ibatis.annotations.*;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface PrivilegedUserMapper {
|
public interface PrivilegedUserMapper {
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ public interface PrivilegedUserMapper {
|
|||||||
@Result(property = "permissions", column = "id", javaType = List.class,
|
@Result(property = "permissions", column = "id", javaType = List.class,
|
||||||
many = @Many(select = "getPermissionsForUser"))
|
many = @Many(select = "getPermissionsForUser"))
|
||||||
})
|
})
|
||||||
PrivilegedUser getUserByUuid(@Param("uuid") String uuid);
|
Optional<PrivilegedUser> getUserByUuid(@Param("uuid") String uuid);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all privileged users with their permissions
|
* Retrieves all privileged users with their permissions
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export const routes: Routes = [
|
|||||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'particles',
|
path: 'login',
|
||||||
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
loadComponent: () => import('./login/login.component').then(m => m.LoginDialogComponent)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<div class="card-div">
|
||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>Frames</mat-card-title>
|
<mat-card-title>Frames</mat-card-title>
|
||||||
@@ -11,11 +12,16 @@
|
|||||||
<h3>Particles in {{ frameId }}</h3>
|
<h3>Particles in {{ frameId }}</h3>
|
||||||
<div class="particles-list">
|
<div class="particles-list">
|
||||||
<div *ngFor="let particle of particleData.frames[frameId]; let i = index" class="particle-item">
|
<div *ngFor="let particle of particleData.frames[frameId]; let i = index" class="particle-item">
|
||||||
<span>Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
|
<span class="particle-item-text">
|
||||||
, {{ particle.z.toFixed(2) }})</span>
|
Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
|
||||||
<button mat-icon-button color="warn" (click)="removeParticle(frameId, i)">
|
, {{ particle.z.toFixed(2) }})
|
||||||
|
</span>
|
||||||
|
<button mat-icon-button (click)="removeParticle(frameId, i)">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-icon-button (click)="highlightParticle(frameId, i)">
|
||||||
|
<mat-icon>lightbulb</mat-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!particleData.frames[frameId] || particleData.frames[frameId].length === 0"
|
<div *ngIf="!particleData.frames[frameId] || particleData.frames[frameId].length === 0"
|
||||||
class="no-particles">
|
class="no-particles">
|
||||||
@@ -39,3 +45,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -17,10 +17,14 @@
|
|||||||
|
|
||||||
.particle-item {
|
.particle-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-item-text {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.particle-item:last-child {
|
.particle-item:last-child {
|
||||||
@@ -30,7 +34,7 @@
|
|||||||
.no-particles {
|
.no-particles {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #888;
|
color: var(--color-primairy);
|
||||||
}
|
}
|
||||||
|
|
||||||
.frame-actions {
|
.frame-actions {
|
||||||
@@ -44,3 +48,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-div {
|
||||||
|
mat-card {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,4 +83,7 @@ export class FramesComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public highlightParticle(frameId: string, i: number) {
|
||||||
|
this.particleManagerService.highlightParticle(frameId, i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,37 @@
|
|||||||
<mat-card class="color-picker-card">
|
<div class="card-div">
|
||||||
|
<mat-card class="particle-card">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>Particle Color</mat-card-title>
|
<mat-card-title>Particle Properties</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
|
<div class="particle-properties">
|
||||||
|
<div class="property-row">
|
||||||
<div class="color-picker">
|
<div class="color-picker">
|
||||||
<input type="color" [(ngModel)]="selectedColor">
|
<input type="color" [(ngModel)]="selectedColor">
|
||||||
<span>Selected Color: {{ selectedColor }}</span>
|
<span>Current color: {{ selectedColor }}</span>
|
||||||
|
</div>
|
||||||
|
<mat-form-field appearance="fill" class="type-field">
|
||||||
|
<mat-label>Select Particle Type</mat-label>
|
||||||
|
<input type="text"
|
||||||
|
placeholder="Search for a particle type"
|
||||||
|
matInput
|
||||||
|
[formControl]="particleTypeControl"
|
||||||
|
[matAutocomplete]="auto">
|
||||||
|
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
|
||||||
|
<mat-option *ngFor="let type of filteredParticleTypes | async" [value]="type">
|
||||||
|
{{ type }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="size-slider">
|
||||||
|
<mat-slider min="0.1" max="4" step="0.1" class="full-width">
|
||||||
|
<input matSliderThumb [(ngModel)]="selectedSize">
|
||||||
|
</mat-slider>
|
||||||
|
<span>Size: {{ selectedSize }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,61 @@
|
|||||||
.color-picker-card {
|
.particle-card {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-properties {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 20ch;
|
||||||
|
max-width: 40ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker {
|
.color-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker input[type="color"] {
|
.color-picker input[type="color"] {
|
||||||
width: 50px;
|
width: 40px;
|
||||||
height: 50px;
|
height: 40px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-div {
|
||||||
|
mat-card {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-slider {
|
||||||
|
width: 95%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--font-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
import {ParticleManagerService} from '../../services/particle-manager.service';
|
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||||
|
import {Particle} from '../../models/particle.model';
|
||||||
|
import {MatSliderModule} from '@angular/material/slider';
|
||||||
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatAutocompleteModule} from '@angular/material/autocomplete';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {map, startWith} from 'rxjs/operators';
|
||||||
|
import {AsyncPipe, NgForOf} from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-particle',
|
selector: 'app-particle',
|
||||||
@@ -11,16 +20,55 @@ import {ParticleManagerService} from '../../services/particle-manager.service';
|
|||||||
MatCardHeader,
|
MatCardHeader,
|
||||||
MatCardTitle,
|
MatCardTitle,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormsModule
|
FormsModule,
|
||||||
|
MatSliderModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
|
NgForOf,
|
||||||
|
AsyncPipe
|
||||||
],
|
],
|
||||||
templateUrl: './particle.component.html',
|
templateUrl: './particle.component.html',
|
||||||
styleUrl: './particle.component.scss'
|
styleUrl: './particle.component.scss'
|
||||||
})
|
})
|
||||||
export class ParticleComponent {
|
export class ParticleComponent implements OnInit {
|
||||||
|
// Available particle types from the enum
|
||||||
|
particleTypes = Object.values(Particle);
|
||||||
|
|
||||||
|
// Form control for the particle type dropdown with filtering
|
||||||
|
particleTypeControl = new FormControl();
|
||||||
|
filteredParticleTypes: Observable<string[]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private particleManagerService: ParticleManagerService,
|
private particleManagerService: ParticleManagerService,
|
||||||
) {
|
) {
|
||||||
|
this.filteredParticleTypes = this.particleTypeControl.valueChanges.pipe(
|
||||||
|
startWith(''),
|
||||||
|
map(value => this._filterParticleTypes(value || ''))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Initialize the particle type control with the current value
|
||||||
|
this.particleTypeControl.setValue(this.selectedParticle);
|
||||||
|
|
||||||
|
// Update the selected particle when the control value changes
|
||||||
|
this.particleTypeControl.valueChanges.subscribe(value => {
|
||||||
|
if (value && Object.values(Particle).includes(value)) {
|
||||||
|
this.selectedParticle = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterParticleTypes(value: string): string[] {
|
||||||
|
const filterValue = value.toLowerCase();
|
||||||
|
return this.particleTypes.filter(type => type.toLowerCase().includes(filterValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display function for the autocomplete
|
||||||
|
displayFn(particle: string): string {
|
||||||
|
return particle ? particle : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,4 +85,31 @@ export class ParticleComponent {
|
|||||||
this.particleManagerService.setSelectedColor(color);
|
this.particleManagerService.setSelectedColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the selected particle type
|
||||||
|
*/
|
||||||
|
public get selectedParticle(): Particle {
|
||||||
|
return this.particleManagerService.particle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the selected particle type
|
||||||
|
*/
|
||||||
|
public set selectedParticle(particle: Particle) {
|
||||||
|
this.particleManagerService.particle = particle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the selected particle size
|
||||||
|
*/
|
||||||
|
public get selectedSize(): number {
|
||||||
|
return this.particleManagerService.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the selected particle size
|
||||||
|
*/
|
||||||
|
public set selectedSize(size: number) {
|
||||||
|
this.particleManagerService.size = size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<div class="card-div">
|
||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title>Particle Properties</mat-card-title>
|
<mat-card-title>Particle Properties</mat-card-title>
|
||||||
@@ -85,7 +86,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<mat-checkbox [(ngModel)]="particleData.stationary">Stationary</mat-checkbox>
|
<mat-checkbox [(ngModel)]="particleData.stationary"><span>Stationary</span></mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.form-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-div {
|
||||||
|
mat-card {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
|||||||
+13
@@ -1,9 +1,22 @@
|
|||||||
<div #rendererContainer class="renderer-container">
|
<div #rendererContainer class="renderer-container">
|
||||||
<div class="plane-controls-overlay">
|
<div class="plane-controls-overlay">
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" style="width: 10ch">
|
||||||
|
<mat-label>Opacity</mat-label>
|
||||||
|
<input matInput type="number" [(ngModel)]="opacity" min="0" max="1" step="0.01" placeholder="">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<button mat-mini-fab color="primary" (click)="resetCamera()"
|
||||||
|
matTooltip="Reset camera">
|
||||||
|
<mat-icon>location_searching</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button mat-mini-fab color="primary" (click)="togglePlaneLock()"
|
<button mat-mini-fab color="primary" (click)="togglePlaneLock()"
|
||||||
[matTooltip]="isPlaneLocked ? 'Unlock Plane' : 'Lock Plane'">
|
[matTooltip]="isPlaneLocked ? 'Unlock Plane' : 'Lock Plane'">
|
||||||
<mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
|
<mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isPlaneLocked" class="plane-orientation-buttons">
|
<div *ngIf="isPlaneLocked" class="plane-orientation-buttons">
|
||||||
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)"
|
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)"
|
||||||
|
|||||||
+9
-2
@@ -3,7 +3,7 @@
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #f0f0f0;
|
background-color: var(--color-primairy);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -21,6 +21,13 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.plane-orientation-buttons {
|
.plane-orientation-buttons {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
@@ -42,5 +49,5 @@
|
|||||||
.plane-orientation-buttons button.active {
|
.plane-orientation-buttons button.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
box-shadow: 0 0 10px var(--color-tertiary);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -7,6 +7,8 @@ import {MatTooltip} from '@angular/material/tooltip';
|
|||||||
import {RendererService} from '../../services/renderer.service';
|
import {RendererService} from '../../services/renderer.service';
|
||||||
import {PlayerModelService} from '../../services/player-model.service';
|
import {PlayerModelService} from '../../services/player-model.service';
|
||||||
import {InputHandlerService} from '../../services/input-handler.service';
|
import {InputHandlerService} from '../../services/input-handler.service';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-render-container',
|
selector: 'app-render-container',
|
||||||
@@ -14,7 +16,11 @@ import {InputHandlerService} from '../../services/input-handler.service';
|
|||||||
MatIcon,
|
MatIcon,
|
||||||
MatMiniFabButton,
|
MatMiniFabButton,
|
||||||
MatTooltip,
|
MatTooltip,
|
||||||
NgIf
|
NgIf,
|
||||||
|
FormsModule,
|
||||||
|
MatInput,
|
||||||
|
MatFormField,
|
||||||
|
MatLabel
|
||||||
],
|
],
|
||||||
templateUrl: './render-container.component.html',
|
templateUrl: './render-container.component.html',
|
||||||
styleUrl: './render-container.component.scss'
|
styleUrl: './render-container.component.scss'
|
||||||
@@ -74,6 +80,14 @@ export class RenderContainerComponent implements AfterViewInit, OnDestroy {
|
|||||||
return this.intersectionPlaneService.isPlaneLocked();
|
return this.intersectionPlaneService.isPlaneLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public set opacity(opacity: number) {
|
||||||
|
this.intersectionPlaneService.currentOpacity = opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get opacity(): number {
|
||||||
|
return this.intersectionPlaneService.currentOpacity;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the plane locked state
|
* Toggle the plane locked state
|
||||||
*/
|
*/
|
||||||
@@ -82,6 +96,10 @@ export class RenderContainerComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.intersectionPlaneService.setPlaneLocked(newLockedState);
|
this.intersectionPlaneService.setPlaneLocked(newLockedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public resetCamera(): void {
|
||||||
|
this.rendererService.resetCamera();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current plane orientation
|
* Get the current plane orientation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* Defines the types of particles available in the system
|
* Defines the types of particles available in the system
|
||||||
*/
|
*/
|
||||||
export enum ParticleType {
|
export enum Particle {
|
||||||
REDSTONE = 'REDSTONE',
|
DUST = 'DUST',
|
||||||
|
DUST_COLOR_TRANSITION = 'DUST_COLOR_TRANSITION',
|
||||||
|
TINTED_LEAVES = 'TINTED_LEAVES'
|
||||||
// Other particle types can be added later
|
// Other particle types can be added later
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ParticleType {
|
||||||
|
HEAD = 'HEAD',
|
||||||
|
TRAIL = 'TRAIL',
|
||||||
|
BREAK_PLACE_BLOCK = 'BREAK_PLACE_BLOCK',
|
||||||
|
DEATH = 'DEATH',
|
||||||
|
KILL = 'KILL',
|
||||||
|
CLICK_BLOCK = 'CLICK_BLOCK',
|
||||||
|
TELEPORT_ARRIVE = 'TELEPORT',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a single particle's information
|
* Represents a single particle's information
|
||||||
*/
|
*/
|
||||||
export interface ParticleInfo {
|
export interface ParticleInfo {
|
||||||
particle_type: string;
|
particle_type: Particle;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
color: string;
|
extra?: number;
|
||||||
extra: number;
|
color?: string;
|
||||||
|
color_gradient_end?: string;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,10 +16,14 @@
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label, span {
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
|
||||||
.plane-controls {
|
.plane-controls {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #f5f5f5;
|
background-color: var(--color-primairy);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -30,10 +34,92 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
:host ::ng-deep {
|
||||||
margin-bottom: 15px;
|
.mdc-text-field--outlined {
|
||||||
|
background-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-form-field {
|
.mdc-text-field--outlined .mdc-floating-label,
|
||||||
width: 100%;
|
.mdc-text-field--outlined .mdc-text-field__input,
|
||||||
|
.mat-mdc-form-field-label,
|
||||||
|
.mat-mdc-select-value-text,
|
||||||
|
.mat-mdc-select-arrow,
|
||||||
|
.mat-mdc-checkbox-label,
|
||||||
|
.mat-mdc-card-header,
|
||||||
|
.mat-mdc-card-title,
|
||||||
|
.mat-mdc-card-content {
|
||||||
|
color: var(--font-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading,
|
||||||
|
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch,
|
||||||
|
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing {
|
||||||
|
border-color: var(--font-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix for dropdown menu
|
||||||
|
.mat-mdc-select-panel {
|
||||||
|
background-color: var(--color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-option {
|
||||||
|
color: var(--font-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-option:hover:not(.mdc-list-item--disabled) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-option.mat-mdc-option-active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-select-panel {
|
||||||
|
background-color: var(--color-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-option {
|
||||||
|
color: var(--font-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-option:hover:not(.mdc-list-item--disabled) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-option.mat-mdc-option-active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab {
|
||||||
|
color: var(--font-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab-header {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab-label-container {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-tab__content .mdc-tab__text-label {
|
||||||
|
color: var(--font-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab-label, .mat-mdc-tab-link {
|
||||||
|
color: var(--font-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab-group.mat-primary .mat-mdc-tab:not(.mat-mdc-tab-disabled) .mdc-tab-indicator__content--underline {
|
||||||
|
border-color: var(--font-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-tab-body-content {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
color: var(--font-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {ParticleComponent} from './components/particle/particle.component';
|
|||||||
import {FramesComponent} from './components/frames/frames.component';
|
import {FramesComponent} from './components/frames/frames.component';
|
||||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||||
import {RenderContainerComponent} from './components/render-container/render-container.component';
|
import {RenderContainerComponent} from './components/render-container/render-container.component';
|
||||||
|
import {ParticlesService} from '../../api';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-particles',
|
selector: 'app-particles',
|
||||||
@@ -55,6 +56,7 @@ export class ParticlesComponent {
|
|||||||
private intersectionPlaneService: IntersectionPlaneService,
|
private intersectionPlaneService: IntersectionPlaneService,
|
||||||
private particleManagerService: ParticleManagerService,
|
private particleManagerService: ParticleManagerService,
|
||||||
private matSnackBar: MatSnackBar,
|
private matSnackBar: MatSnackBar,
|
||||||
|
private particlesService: ParticlesService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +95,37 @@ export class ParticlesComponent {
|
|||||||
return this.particleManagerService.generateJson();
|
return this.particleManagerService.generateJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getJsonFile(): Blob {
|
||||||
|
const jsonContent = this.generateJson();
|
||||||
|
return new Blob([jsonContent], {type: 'application/json'});
|
||||||
|
}
|
||||||
|
|
||||||
|
public saveJsonToFile(): void {
|
||||||
|
const jsonContent = this.generateJson();
|
||||||
|
const blob = new Blob([jsonContent], {type: 'application/json'});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create a temporary link element
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'particle-data.json';
|
||||||
|
|
||||||
|
// Append to the document, click it, and remove it
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
this.matSnackBar.open('JSON file downloaded', '', {duration: 2000});
|
||||||
|
}
|
||||||
|
|
||||||
public copyJson() {
|
public copyJson() {
|
||||||
navigator.clipboard.writeText(this.generateJson()).then(() => {
|
navigator.clipboard.writeText(this.generateJson()).then(() => {
|
||||||
this.matSnackBar.open('Copied to clipboard', '', {duration: 2000})
|
this.matSnackBar.open('Copied to clipboard', '', {duration: 2000})
|
||||||
});
|
});
|
||||||
|
//TODO validation
|
||||||
|
this.particlesService.saveFile(this.particleManagerService.getParticleData().particle_name, this.getJsonFile());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export class IntersectionPlaneService {
|
|||||||
private planePosition: number = 0; // Position in 1/16th of a block
|
private planePosition: number = 0; // Position in 1/16th of a block
|
||||||
private currentOrientation: PlaneOrientation = PlaneOrientation.HORIZONTAL_FRONT;
|
private currentOrientation: PlaneOrientation = PlaneOrientation.HORIZONTAL_FRONT;
|
||||||
private planeLocked: boolean = false;
|
private planeLocked: boolean = false;
|
||||||
|
private opacity: number = 0.05;
|
||||||
|
|
||||||
constructor(private rendererService: RendererService) {
|
constructor(private rendererService: RendererService) {
|
||||||
}
|
}
|
||||||
@@ -152,11 +153,19 @@ export class IntersectionPlaneService {
|
|||||||
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
|
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
|
||||||
color: color,
|
color: color,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.05,
|
opacity: this.opacity,
|
||||||
side: THREE.DoubleSide
|
side: THREE.DoubleSide
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public set currentOpacity(opacity: number) {
|
||||||
|
this.opacity = opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get currentOpacity(): number {
|
||||||
|
return this.opacity;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the intersection plane
|
* Gets the intersection plane
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import {RendererService} from './renderer.service';
|
import {RendererService} from './renderer.service';
|
||||||
import { ParticleData, ParticleInfo, ParticleType } from '../models/particle.model';
|
import {Particle, ParticleData, ParticleInfo, ParticleType} from '../models/particle.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service responsible for managing particles in the scene
|
* Service responsible for managing particles in the scene
|
||||||
@@ -14,9 +14,9 @@ export class ParticleManagerService {
|
|||||||
private particleData: ParticleData = {
|
private particleData: ParticleData = {
|
||||||
particle_name: '',
|
particle_name: '',
|
||||||
display_name: '',
|
display_name: '',
|
||||||
particle_type: ParticleType.REDSTONE,
|
particle_type: ParticleType.TRAIL,
|
||||||
lore: '',
|
lore: '',
|
||||||
display_item: 'REDSTONE',
|
display_item: 'DIRT',
|
||||||
permission: '',
|
permission: '',
|
||||||
package_permission: '',
|
package_permission: '',
|
||||||
frame_delay: 1,
|
frame_delay: 1,
|
||||||
@@ -31,15 +31,18 @@ export class ParticleManagerService {
|
|||||||
private currentFrame: string = 'frame1';
|
private currentFrame: string = 'frame1';
|
||||||
private frames: string[] = ['frame1'];
|
private frames: string[] = ['frame1'];
|
||||||
private selectedColor: string = '#ff0000';
|
private selectedColor: string = '#ff0000';
|
||||||
|
private selectedParticle: Particle = Particle.DUST;
|
||||||
|
private selectedSize: number = 1;
|
||||||
|
|
||||||
constructor(private rendererService: RendererService) {}
|
constructor(private rendererService: RendererService) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a particle at the specified position
|
* Adds a particle at the specified position
|
||||||
*/
|
*/
|
||||||
addParticle(x: number, y: number, z: number): void {
|
addParticle(x: number, y: number, z: number): void {
|
||||||
// Create a visual representation of the particle
|
// Create a visual representation of the particle
|
||||||
const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16);
|
const particleGeometry = new THREE.SphereGeometry(0.03 * this.selectedSize, 16, 16);
|
||||||
const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor});
|
const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor});
|
||||||
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||||
|
|
||||||
@@ -49,17 +52,17 @@ export class ParticleManagerService {
|
|||||||
|
|
||||||
// Add to particle data
|
// Add to particle data
|
||||||
const hexColor = this.selectedColor.replace('#', '');
|
const hexColor = this.selectedColor.replace('#', '');
|
||||||
const r = parseInt(hexColor.substring(0, 2), 16) / 255;
|
|
||||||
const g = parseInt(hexColor.substring(2, 4), 16) / 255;
|
|
||||||
const b = parseInt(hexColor.substring(4, 6), 16) / 255;
|
|
||||||
|
|
||||||
|
//TODO make this work for more than just type DUST
|
||||||
const particleInfo: ParticleInfo = {
|
const particleInfo: ParticleInfo = {
|
||||||
particle_type: ParticleType.REDSTONE,
|
particle_type: this.selectedParticle,
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
z: z,
|
z: z,
|
||||||
color: `${r},${g},${b}`,
|
color: hexColor,
|
||||||
extra: 1
|
// color_gradient_end: hexColor2,
|
||||||
|
extra: 1,
|
||||||
|
size: this.selectedSize
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.particleData.frames[this.currentFrame]) {
|
if (!this.particleData.frames[this.currentFrame]) {
|
||||||
@@ -86,16 +89,9 @@ export class ParticleManagerService {
|
|||||||
if (!this.particleData.frames[frameId]) return;
|
if (!this.particleData.frames[frameId]) return;
|
||||||
|
|
||||||
for (const particleInfo of this.particleData.frames[frameId]) {
|
for (const particleInfo of this.particleData.frames[frameId]) {
|
||||||
const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16);
|
const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
|
||||||
|
|
||||||
// Parse color
|
|
||||||
const colorParts = particleInfo.color.split(',');
|
|
||||||
const color = new THREE.Color(
|
|
||||||
parseFloat(colorParts[0]),
|
|
||||||
parseFloat(colorParts[1]),
|
|
||||||
parseFloat(colorParts[2])
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const color = this.getColor(particleInfo);
|
||||||
const particleMaterial = new THREE.MeshBasicMaterial({color});
|
const particleMaterial = new THREE.MeshBasicMaterial({color});
|
||||||
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||||
|
|
||||||
@@ -120,6 +116,57 @@ export class ParticleManagerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
highlightParticle(frameId: string, index: number): void {
|
||||||
|
if (!(this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const particleInfo = this.particleData.frames[frameId][index];
|
||||||
|
const color = this.getColor(particleInfo);
|
||||||
|
const particleMaterial = new THREE.MeshBasicMaterial({color});
|
||||||
|
const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
|
||||||
|
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||||
|
particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
|
||||||
|
this.rendererService.scene.add(particleMesh);
|
||||||
|
this.particles.push(particleMesh);
|
||||||
|
this.animatePulse(particleMesh, 3, () => {
|
||||||
|
this.rendererService.scene.remove(particleMesh);
|
||||||
|
this.clearParticleVisuals();
|
||||||
|
this.renderFrameParticles(this.currentFrame);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private animatePulse(mesh: THREE.Mesh, cycles: number, onComplete: () => void): void {
|
||||||
|
const duration = 300;
|
||||||
|
const maxScale = 0.08 / 0.03;
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
const animate = (time: number) => {
|
||||||
|
const elapsed = (time - startTime) % duration;
|
||||||
|
const t = elapsed / (duration / 2);
|
||||||
|
const scaleFactor = t <= 1 ? 1 + (maxScale - 1) * t : maxScale - (maxScale - 1) * (t - 1);
|
||||||
|
mesh.scale.setScalar(scaleFactor);
|
||||||
|
|
||||||
|
if (time - startTime >= duration * cycles) {
|
||||||
|
onComplete();
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getColor(particleInfo: ParticleInfo) {
|
||||||
|
if (particleInfo.color) {
|
||||||
|
const r = parseInt(particleInfo.color.substring(0, 2), 16) / 255;
|
||||||
|
const g = parseInt(particleInfo.color.substring(2, 4), 16) / 255;
|
||||||
|
const b = parseInt(particleInfo.color.substring(4, 6), 16) / 255;
|
||||||
|
return new THREE.Color(r, g, b);
|
||||||
|
} else {
|
||||||
|
return new THREE.Color(255, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the selected color for new particles
|
* Sets the selected color for new particles
|
||||||
*/
|
*/
|
||||||
@@ -134,6 +181,22 @@ export class ParticleManagerService {
|
|||||||
return this.selectedColor;
|
return this.selectedColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get particle(): Particle {
|
||||||
|
return this.selectedParticle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set particle(selectedParticle: Particle) {
|
||||||
|
this.selectedParticle = selectedParticle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get size(): number {
|
||||||
|
return this.selectedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set size(selectedSize: number) {
|
||||||
|
this.selectedSize = selectedSize;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the particle data
|
* Gets the particle data
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {ElementRef, Injectable} from '@angular/core';
|
import {ElementRef, Injectable} from '@angular/core';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
|
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
import {ThemeService} from '../../theme/theme.service';
|
||||||
|
import {THEME_MODE} from '../../constant';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service responsible for managing the Three.js rendering environment
|
* Service responsible for managing the Three.js rendering environment
|
||||||
@@ -13,6 +15,16 @@ export class RendererService {
|
|||||||
camera!: THREE.PerspectiveCamera;
|
camera!: THREE.PerspectiveCamera;
|
||||||
renderer!: THREE.WebGLRenderer;
|
renderer!: THREE.WebGLRenderer;
|
||||||
controls!: OrbitControls;
|
controls!: OrbitControls;
|
||||||
|
private currentTheme: THEME_MODE = THEME_MODE.LIGHT;
|
||||||
|
|
||||||
|
constructor(private themeService: ThemeService) {
|
||||||
|
this.themeService.theme$.subscribe(theme => {
|
||||||
|
this.currentTheme = theme;
|
||||||
|
if (this.scene) {
|
||||||
|
this.setBackgroundColor(theme);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the Three.js scene, camera, renderer, and controls
|
* Initializes the Three.js scene, camera, renderer, and controls
|
||||||
@@ -20,7 +32,7 @@ export class RendererService {
|
|||||||
initializeRenderer(container: ElementRef): void {
|
initializeRenderer(container: ElementRef): void {
|
||||||
// Create scene
|
// Create scene
|
||||||
this.scene = new THREE.Scene();
|
this.scene = new THREE.Scene();
|
||||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
this.setBackgroundColor(this.currentTheme);
|
||||||
|
|
||||||
// Get container dimensions
|
// Get container dimensions
|
||||||
const containerWidth = container.nativeElement.clientWidth;
|
const containerWidth = container.nativeElement.clientWidth;
|
||||||
@@ -54,6 +66,20 @@ export class RendererService {
|
|||||||
this.addLights();
|
this.addLights();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setBackgroundColor(theme: THEME_MODE) {
|
||||||
|
this.scene.background = new THREE.Color(this.currentTheme === THEME_MODE.DARK ? 0x242526 : 0xFBFBFE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the camera to its default position and orientation.
|
||||||
|
*/
|
||||||
|
public resetCamera(): void {
|
||||||
|
this.camera.position.set(-1, 2, 3);
|
||||||
|
this.camera.lookAt(0, 1, 0);
|
||||||
|
this.controls.target.set(0, 1, 0);
|
||||||
|
this.controls.update();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds lighting to the scene
|
* Adds lighting to the scene
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ components:
|
|||||||
schemas:
|
schemas:
|
||||||
PermissionClaim:
|
PermissionClaim:
|
||||||
$ref: './schemas/permissions/permissions.yml#/components/schemas/PermissionClaim'
|
$ref: './schemas/permissions/permissions.yml#/components/schemas/PermissionClaim'
|
||||||
|
securitySchemes:
|
||||||
|
bearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
tags:
|
tags:
|
||||||
- name: history
|
- name: history
|
||||||
description: Retrieves punishment history
|
description: Retrieves punishment history
|
||||||
- name: team
|
- name: team
|
||||||
description: Retrieves information about the staff team
|
description: Retrieves information about the staff team
|
||||||
|
- name: particles
|
||||||
|
description: All actions related to particles
|
||||||
paths:
|
paths:
|
||||||
/team/{team}:
|
/team/{team}:
|
||||||
$ref: './schemas/team/team.yml#/getTeam'
|
$ref: './schemas/team/team.yml#/getTeam'
|
||||||
@@ -46,3 +53,11 @@ paths:
|
|||||||
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
|
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
|
||||||
/login/userLogin/{code}:
|
/login/userLogin/{code}:
|
||||||
$ref: './schemas/login/login.yml#/UserLogin'
|
$ref: './schemas/login/login.yml#/UserLogin'
|
||||||
|
/files/save/{filename}:
|
||||||
|
$ref: './schemas/particles/particles.yml#/SaveFile'
|
||||||
|
/files/save/{uuid}/{filename}:
|
||||||
|
$ref: './schemas/particles/particles.yml#/SaveFileForUser'
|
||||||
|
/files/download/{filename}/{secret}:
|
||||||
|
$ref: './schemas/particles/particles.yml#/DownloadFile'
|
||||||
|
/files/download/{uuid}/{filename}:
|
||||||
|
$ref: './schemas/particles/particles.yml#/DownloadFileForUser'
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
components:
|
||||||
|
parameters:
|
||||||
|
Filename:
|
||||||
|
name: filename
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The name of the file
|
||||||
|
Secret:
|
||||||
|
name: Authorization
|
||||||
|
in: header
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Secret
|
||||||
|
schemas:
|
||||||
|
FileData:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- content
|
||||||
|
properties:
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
description: The content of the file
|
||||||
|
|
||||||
|
SaveFile:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- particles
|
||||||
|
summary: Save a file
|
||||||
|
description: Save a file to the server (requires authorization)
|
||||||
|
operationId: saveFile
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/Filename'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FileData'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: File saved successfully
|
||||||
|
'401':
|
||||||
|
description: Unauthorized - Invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
'403':
|
||||||
|
description: Forbidden - Insufficient permissions
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
|
||||||
|
SaveFileForUser:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- particles
|
||||||
|
summary: Save a file for a specific user
|
||||||
|
description: Save a file to the server for a specific user (requires head_mod permission)
|
||||||
|
operationId: saveFileForUser
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||||
|
- $ref: '#/components/parameters/Filename'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FileData'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: File saved successfully
|
||||||
|
'401':
|
||||||
|
description: Unauthorized - Invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
'403':
|
||||||
|
description: Forbidden - Insufficient permissions
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
|
||||||
|
DownloadFile:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- particles
|
||||||
|
summary: Download a file
|
||||||
|
description: Download a file from the server using a secret key
|
||||||
|
operationId: downloadFile
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/Secret'
|
||||||
|
- $ref: '#/components/parameters/Filename'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: File downloaded successfully
|
||||||
|
content:
|
||||||
|
application/octet-stream:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
'404':
|
||||||
|
description: File not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
|
||||||
|
DownloadFileForUser:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- particles
|
||||||
|
summary: Download a file for a specific user
|
||||||
|
description: Download a file from the server for a specific user (requires authorization)
|
||||||
|
operationId: downloadFileForUser
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/Secret'
|
||||||
|
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
|
||||||
|
- $ref: '#/components/parameters/Filename'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: File downloaded successfully
|
||||||
|
content:
|
||||||
|
application/octet-stream:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
'401':
|
||||||
|
description: Unauthorized - Invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
'404':
|
||||||
|
description: File not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
|
default:
|
||||||
|
description: Unexpected error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../generic/errors.yml#/components/schemas/ApiError'
|
||||||
Reference in New Issue
Block a user