working, deployed
This commit is contained in:
commit
31f6295833
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
12
application.properties
Normal file
12
application.properties
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
logging.level.de.ph87=DEBUG
|
||||||
|
#-
|
||||||
|
spring.datasource.url=jdbc:h2:./database;AUTO_SERVER=TRUE
|
||||||
|
spring.datasource.driverClassName=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=password
|
||||||
|
#-
|
||||||
|
spring.jpa.hibernate.ddl-auto=create
|
||||||
|
#-
|
||||||
|
spring.jackson.serialization.indent_output=true
|
||||||
|
#-
|
||||||
|
server.port=8084
|
||||||
41
deploy.sh
Normal file
41
deploy.sh
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
echo "| Building backend |"
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
|
||||||
|
mvn clean install || exit 1
|
||||||
|
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
echo "| Uploading backend |"
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
|
||||||
|
rsync --archive -e 'ssh -p 2222' ./target/IsabellTimo.jar mc@mc.ph87.de:/srv/IsabellTimo/backend/ || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
echo "| Building frontend |"
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
|
||||||
|
cd src/main/angular || exit 1
|
||||||
|
|
||||||
|
npm run build || exit 1
|
||||||
|
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
echo "| Uploading frontend |"
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
|
||||||
|
rsync --archive --delete -e 'ssh -p 2222' ./dist/angular/browser/ mc@mc.ph87.de:/srv/IsabellTimo/frontend/ || exit 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
echo "| Restarting docker |"
|
||||||
|
echo "+---------------------------------------------------+"
|
||||||
|
|
||||||
|
ssh root@mc.ph87.de -p 2222 '/srv/IsabellTimo/restart.sh' || exit 1
|
||||||
63
pom.xml
Normal file
63
pom.xml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>de.ph87</groupId>
|
||||||
|
<artifactId>IsabellTimo</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.4.6</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mortennobel</groupId>
|
||||||
|
<artifactId>java-image-scaling</artifactId>
|
||||||
|
<version>0.8.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.drewnoakes</groupId>
|
||||||
|
<artifactId>metadata-extractor</artifactId>
|
||||||
|
<version>2.19.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>IsabellTimo</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
17
src/main/angular/.editorconfig
Normal file
17
src/main/angular/.editorconfig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
42
src/main/angular/.gitignore
vendored
Normal file
42
src/main/angular/.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
59
src/main/angular/README.md
Normal file
59
src/main/angular/README.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# IsabellTimo
|
||||||
|
|
||||||
|
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.12.
|
||||||
|
|
||||||
|
## Development server
|
||||||
|
|
||||||
|
To start a local development server, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||||
|
|
||||||
|
## Code scaffolding
|
||||||
|
|
||||||
|
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate component component-name
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng generate --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build the project run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
For end-to-end (e2e) testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||||
124
src/main/angular/angular.json
Normal file
124
src/main/angular/angular.json
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"IsabellTimo": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "less",
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:class": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:guard": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:interceptor": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:pipe": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:resolver": {
|
||||||
|
"skipTests": true
|
||||||
|
},
|
||||||
|
"@schematics/angular:service": {
|
||||||
|
"skipTests": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/angular",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "less",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.less"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kB",
|
||||||
|
"maximumError": "1MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "IsabellTimo:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "IsabellTimo:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "less",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.less"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14908
src/main/angular/package-lock.json
generated
Normal file
14908
src/main/angular/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
src/main/angular/package.json
Normal file
37
src/main/angular/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "angular",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/common": "^19.2.0",
|
||||||
|
"@angular/compiler": "^19.2.0",
|
||||||
|
"@angular/core": "^19.2.0",
|
||||||
|
"@angular/forms": "^19.2.0",
|
||||||
|
"@angular/platform-browser": "^19.2.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||||
|
"@angular/router": "^19.2.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^19.2.12",
|
||||||
|
"@angular/cli": "^19.2.12",
|
||||||
|
"@angular/compiler-cli": "^19.2.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"jasmine-core": "~5.6.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/main/angular/public/background.jpg
Normal file
BIN
src/main/angular/public/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
BIN
src/main/angular/public/background.png
Normal file
BIN
src/main/angular/public/background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
src/main/angular/public/favicon.ico
Normal file
BIN
src/main/angular/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
4
src/main/angular/public/placeholder.svg
Normal file
4
src/main/angular/public/placeholder.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="white">
|
||||||
|
<path d="M7.828 5l-1-1H22v15.172l-1-1v-.69l-3.116-3.117-.395.296-.714-.714.854-.64a.503.503 0 0 1 .657.046L21 16.067V5zM3 20v-.519l2.947-2.947a1.506 1.506 0 0 0 .677.163 1.403 1.403 0 0 0 .997-.415l2.916-2.916-.706-.707-2.916 2.916a.474.474 0 0 1-.678-.048.503.503 0 0 0-.704.007L3 18.067V5.828l-1-1V21h16.172l-1-1zM17 8.5A1.5 1.5 0 1 1 15.5 7 1.5 1.5 0 0 1 17 8.5zm-1 0a.5.5 0 1 0-.5.5.5.5 0 0 0 .5-.5zm5.646 13.854l.707-.707-20-20-.707.707z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 568 B |
6
src/main/angular/src/app/app.component.html
Normal file
6
src/main/angular/src/app/app.component.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<div id="title" class="coloredText">
|
||||||
|
<div>Isabell⚭Timo</div>
|
||||||
|
<div>09. August 2025</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-outlet/>
|
||||||
6
src/main/angular/src/app/app.component.less
Normal file
6
src/main/angular/src/app/app.component.less
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@import "../config";
|
||||||
|
|
||||||
|
#title {
|
||||||
|
font-size: 150%;
|
||||||
|
margin-bottom: @space;
|
||||||
|
}
|
||||||
12
src/main/angular/src/app/app.component.ts
Normal file
12
src/main/angular/src/app/app.component.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.less'
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'IsabellTimo';
|
||||||
|
}
|
||||||
13
src/main/angular/src/app/app.config.ts
Normal file
13
src/main/angular/src/app/app.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||||
|
import {provideRouter} from '@angular/router';
|
||||||
|
|
||||||
|
import {routes} from './app.routes';
|
||||||
|
import {provideHttpClient} from '@angular/common/http';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideZoneChangeDetection({eventCoalescing: true}),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(),
|
||||||
|
]
|
||||||
|
};
|
||||||
10
src/main/angular/src/app/app.routes.ts
Normal file
10
src/main/angular/src/app/app.routes.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {Routes} from '@angular/router';
|
||||||
|
import {UploadComponent} from './upload/upload.component';
|
||||||
|
import {GalleryComponent} from './gallery/gallery.component';
|
||||||
|
import {PendingChangesGuard} from './upload/pending.guard';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{path: 'gallery', component: GalleryComponent},
|
||||||
|
{path: '', component: UploadComponent, canDeactivate: [PendingChangesGuard]},
|
||||||
|
{path: '**', redirectTo: ''},
|
||||||
|
];
|
||||||
32
src/main/angular/src/app/gallery/Picture.ts
Normal file
32
src/main/angular/src/app/gallery/Picture.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {orNull, validateDate, validateString} from '../validators';
|
||||||
|
|
||||||
|
export class Picture {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly uuid: string,
|
||||||
|
readonly originalDate: Date | null,
|
||||||
|
readonly uploadDate: Date,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Picture {
|
||||||
|
return new Picture(
|
||||||
|
validateString(json.uuid),
|
||||||
|
orNull(json.originalDate, validateDate),
|
||||||
|
validateDate(json.uploadDate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byUploadDateDesc(a: Picture, b: Picture): number {
|
||||||
|
return b.uploadDate.getTime() - a.uploadDate.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
static byOriginalDateDesc(a: Picture, b: Picture): number {
|
||||||
|
const aa = a.originalDate === null ? a.uploadDate.getTime() : a.originalDate.getTime();
|
||||||
|
const bb = b.originalDate === null ? b.uploadDate.getTime() : b.originalDate.getTime();
|
||||||
|
return bb - aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
20
src/main/angular/src/app/gallery/gallery.component.html
Normal file
20
src/main/angular/src/app/gallery/gallery.component.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div class="button buttonGoTo" routerLink="">
|
||||||
|
← Foto hochladen
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="button" *ngIf="sortByUpload" (click)="sortByUpload = !sortByUpload">Zuletzt hochgeladen</span>
|
||||||
|
<span class="button" *ngIf="!sortByUpload" (click)="sortByUpload = !sortByUpload">Zuletzt erstellt</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="images" *ngIf="pictures.length > 0">
|
||||||
|
<img [src]="pictureUrl(picture)" *ngFor="let picture of sorted()" alt="" loading="lazy">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="noImages" *ngIf="pictures.length === 0">
|
||||||
|
Noch keine Fotos<br>hochgeladen
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button buttonGoTo" routerLink="">
|
||||||
|
← Foto hochladen
|
||||||
|
</div>
|
||||||
12
src/main/angular/src/app/gallery/gallery.component.less
Normal file
12
src/main/angular/src/app/gallery/gallery.component.less
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@import "../../config";
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 95vw;
|
||||||
|
margin-bottom: calc(@space / 2);
|
||||||
|
margin-left: calc(@space / 2);
|
||||||
|
margin-right: calc(@space / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.noImages {
|
||||||
|
margin: calc(5 * @space) 0;
|
||||||
|
}
|
||||||
41
src/main/angular/src/app/gallery/gallery.component.ts
Normal file
41
src/main/angular/src/app/gallery/gallery.component.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {Picture} from './Picture';
|
||||||
|
import {NgForOf, NgIf} from '@angular/common';
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
import {PictureService} from './picture.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-gallery',
|
||||||
|
imports: [
|
||||||
|
NgForOf,
|
||||||
|
RouterLink,
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './gallery.component.html',
|
||||||
|
styleUrl: './gallery.component.less'
|
||||||
|
})
|
||||||
|
export class GalleryComponent implements OnInit {
|
||||||
|
|
||||||
|
protected pictures: Picture[] = [];
|
||||||
|
|
||||||
|
protected sortByUpload: boolean = true;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly pictureService: PictureService,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.pictureService.all(list => this.pictures = list);
|
||||||
|
}
|
||||||
|
|
||||||
|
pictureUrl(picture: Picture): string {
|
||||||
|
return this.pictureService.apiUrl('http', ['Picture', picture.uuid, 'preview']);
|
||||||
|
}
|
||||||
|
|
||||||
|
sorted(): Picture[] {
|
||||||
|
return this.pictures.sort(this.sortByUpload ? Picture.byUploadDateDesc : Picture.byOriginalDateDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
src/main/angular/src/app/gallery/picture.service.ts
Normal file
33
src/main/angular/src/app/gallery/picture.service.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {map} from "rxjs";
|
||||||
|
import {Picture} from "./Picture";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PictureService {
|
||||||
|
|
||||||
|
readonly secure = location.protocol.endsWith('s:') ? 's' : '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly http: HttpClient,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
all(next: (list: Picture[]) => any): void {
|
||||||
|
this.http.get<any[]>(this.apiUrl("http", ['Picture', 'all'])).pipe(map(list => list.map(Picture.fromJson))).subscribe(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
upload(file: File, success: () => any, error: () => any): void {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('file', file);
|
||||||
|
this.http.post(this.apiUrl("http", ['Picture', 'upload']), form).subscribe({next: success, error: error});
|
||||||
|
}
|
||||||
|
|
||||||
|
apiUrl(protocol: string, path: string[]): string {
|
||||||
|
return `${protocol}${this.secure}://${location.hostname}/api/${path.join('/')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
src/main/angular/src/app/upload/pending.guard.ts
Normal file
21
src/main/angular/src/app/upload/pending.guard.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {CanDeactivate, CanDeactivateFn} from '@angular/router';
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
|
||||||
|
export interface CanComponentDeactivate {
|
||||||
|
|
||||||
|
canDeactivate(): boolean;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pendingGuard: CanDeactivateFn<CanComponentDeactivate> = (component, currentRoute, currentState, nextState) => {
|
||||||
|
return component.canDeactivate();
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
export class PendingChangesGuard implements CanDeactivate<CanComponentDeactivate> {
|
||||||
|
|
||||||
|
canDeactivate(component: CanComponentDeactivate): boolean | Promise<boolean> {
|
||||||
|
return component.canDeactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
src/main/angular/src/app/upload/upload.component.html
Normal file
37
src/main/angular/src/app/upload/upload.component.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<div class="button buttonGoTo" routerLink="/gallery">
|
||||||
|
Zur Galerie →
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<label class="button" [class.hidden]="uploading">
|
||||||
|
<input #input type="file" name="file" accept="image/*" (change)="fileChanged()">
|
||||||
|
Foto auswählen
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="button buttonDisabled" [class.hidden]="!uploading">
|
||||||
|
Foto auswählen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="previewBox">
|
||||||
|
<img #preview id="preview" alt="Vorschau des Bildes" src="placeholder.svg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button" [class.buttonError]="success === false" [class.buttonDisabled]="!selected || uploading" (click)="upload()">
|
||||||
|
<ng-container *ngIf="!uploading">
|
||||||
|
<ng-container *ngIf="success === null">
|
||||||
|
Hochladen
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="success === true">
|
||||||
|
Erfolgreich hochgeladen 😊
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="success === false">
|
||||||
|
Fehler! Nochmal?
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="uploading">
|
||||||
|
Lade hoch...
|
||||||
|
</ng-container>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
26
src/main/angular/src/app/upload/upload.component.less
Normal file
26
src/main/angular/src/app/upload/upload.component.less
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@import "../../config";
|
||||||
|
|
||||||
|
#previewBox {
|
||||||
|
text-align: center;
|
||||||
|
height: 18em;
|
||||||
|
width: 90vw;
|
||||||
|
margin: 0 auto @space;
|
||||||
|
background-color: @previewBack;
|
||||||
|
border: 1px solid @previewBorder;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: @space;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=file] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
87
src/main/angular/src/app/upload/upload.component.ts
Normal file
87
src/main/angular/src/app/upload/upload.component.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import {Component, ElementRef, HostListener, OnDestroy, ViewChild} from '@angular/core';
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
import {CanComponentDeactivate} from './pending.guard';
|
||||||
|
import {NgIf} from '@angular/common';
|
||||||
|
import {PictureService} from '../gallery/picture.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-upload',
|
||||||
|
imports: [
|
||||||
|
RouterLink,
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './upload.component.html',
|
||||||
|
styleUrl: './upload.component.less'
|
||||||
|
})
|
||||||
|
export class UploadComponent implements OnDestroy, CanComponentDeactivate {
|
||||||
|
|
||||||
|
@ViewChild('preview')
|
||||||
|
protected preview!: ElementRef;
|
||||||
|
|
||||||
|
@ViewChild('input')
|
||||||
|
protected input!: ElementRef;
|
||||||
|
|
||||||
|
protected selected: File | null = null;
|
||||||
|
|
||||||
|
protected uploading: boolean = false;
|
||||||
|
|
||||||
|
protected success: boolean | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected imageService: PictureService,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
fileChanged() {
|
||||||
|
this.reset(null);
|
||||||
|
const file = this.input.nativeElement.files?.[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = readerEvent => {
|
||||||
|
this.preview.nativeElement.src = (readerEvent.target?.result || '') as string;
|
||||||
|
this.selected = file;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private reset(success: boolean | null) {
|
||||||
|
this.uploading = false;
|
||||||
|
this.success = success;
|
||||||
|
if (success !== false) {
|
||||||
|
this.selected = null;
|
||||||
|
this.preview.nativeElement.src = 'placeholder.svg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:beforeunload', ['$event'])
|
||||||
|
unloadNotification($event: BeforeUnloadEvent): void {
|
||||||
|
if (this.selected) {
|
||||||
|
$event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canDeactivate(): boolean {
|
||||||
|
if (this.selected) {
|
||||||
|
return confirm('Du hast ein Foto ausgewählt, aber noch nicht hochgeladen. Wirklich verlassen?');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
window.removeEventListener('beforeunload', this.unloadNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
upload() {
|
||||||
|
if (this.uploading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.uploading = true;
|
||||||
|
this.imageService.upload(this.selected, () => this.reset(true), () => this.reset(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
src/main/angular/src/app/validators.ts
Normal file
21
src/main/angular/src/app/validators.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export function validateString(value: any) {
|
||||||
|
if (!(typeof value === 'string')) {
|
||||||
|
throw new Error("Not a string: " + JSON.stringify(value));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDate(value: any): Date {
|
||||||
|
const date = new Date(Date.parse(validateString(value)));
|
||||||
|
if (!date) {
|
||||||
|
throw new Error("Not a date: " + JSON.stringify(value));
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function orNull<T, R>(t: T | null | undefined, map: (t: T) => R): R | null {
|
||||||
|
if (t === null || t === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return map(t);
|
||||||
|
}
|
||||||
7
src/main/angular/src/config.less
Normal file
7
src/main/angular/src/config.less
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@space: 0.5em;
|
||||||
|
|
||||||
|
@magenta: #6d2f47;
|
||||||
|
@gold: #eee08c;
|
||||||
|
|
||||||
|
@previewBack: @magenta;
|
||||||
|
@previewBorder: gray;
|
||||||
13
src/main/angular/src/index.html
Normal file
13
src/main/angular/src/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Isabell ⚭ Timo</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
src/main/angular/src/main.ts
Normal file
6
src/main/angular/src/main.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, appConfig)
|
||||||
|
.catch((err) => console.error(err));
|
||||||
91
src/main/angular/src/styles.less
Normal file
91
src/main/angular/src/styles.less
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
@import "config";
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 3vh;
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
margin: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: #ffc9a3 url("/background.jpg") fixed center top;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, button {
|
||||||
|
all: unset;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coloredText {
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 0.5em @magenta, 0 0 0.5em @magenta, 0 0 0.5em @magenta;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: @space;
|
||||||
|
margin-bottom: @space;
|
||||||
|
cursor: pointer;
|
||||||
|
background: linear-gradient(145deg, #fff4c2 0%, #ffd700 40%, #e6ac00 100%);
|
||||||
|
color: #3b2900;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
box-shadow: inset 0 0.1em 0 rgba(255, 255, 255, 0.6), 0 0.2em 0.4em rgba(0, 0, 0, 0.3);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:not(.buttonDisabled):hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
box-shadow: inset 0 0.1em 0 rgba(255, 255, 255, 0.5), 0 0.25em 0.5em rgba(0, 0, 0, 0.4);
|
||||||
|
transform: translateY(-0.1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:not(.buttonDisabled):active {
|
||||||
|
transform: translateY(0.1em);
|
||||||
|
box-shadow: inset 0 0.2em 0.5em rgba(0, 0, 0, 0.4), 0 0.2em 0.2em rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:not(.buttonDisabled):before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -75%;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(120deg,
|
||||||
|
rgba(255, 255, 255, 0.0) 0%,
|
||||||
|
rgba(255, 255, 255, 0.6) 50%,
|
||||||
|
rgba(255, 255, 255, 0.0) 100%);
|
||||||
|
transform: skewX(-25deg);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:not(.buttonDisabled):hover::before {
|
||||||
|
left: 125%;
|
||||||
|
transition: left 0.75s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonGoTo {
|
||||||
|
background-color: @magenta;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonDisabled {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonError {
|
||||||
|
background: red !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
15
src/main/angular/tsconfig.app.json
Normal file
15
src/main/angular/tsconfig.app.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
src/main/angular/tsconfig.json
Normal file
27
src/main/angular/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/angular/tsconfig.spec.json
Normal file
15
src/main/angular/tsconfig.spec.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
13
src/main/java/de/ph87/isabell_und_timo/Backend.java
Normal file
13
src/main/java/de/ph87/isabell_und_timo/Backend.java
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package de.ph87.isabell_und_timo;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class Backend {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(Backend.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
14
src/main/java/de/ph87/isabell_und_timo/picture/CONFIG.java
Normal file
14
src/main/java/de/ph87/isabell_und_timo/picture/CONFIG.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package de.ph87.isabell_und_timo.picture;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
public class CONFIG {
|
||||||
|
|
||||||
|
public static final int PREVIEW_SIZE = 1000;
|
||||||
|
|
||||||
|
public static final Path RESIZED = Paths.get("images/%d".formatted(PREVIEW_SIZE));
|
||||||
|
|
||||||
|
public static final Path ORIGINAL = Paths.get("images/original");
|
||||||
|
|
||||||
|
}
|
||||||
102
src/main/java/de/ph87/isabell_und_timo/picture/Picture.java
Normal file
102
src/main/java/de/ph87/isabell_und_timo/picture/Picture.java
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package de.ph87.isabell_und_timo.picture;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Version;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Picture {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String uuid = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private long version;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String originalFilename;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private ZonedDateTime originalDate;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private long size;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private int height;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private int width;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private String make;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private String uploadFilename;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private ZonedDateTime uploadDate;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String uploadAddress;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private String uploadAgent;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean visible = true;
|
||||||
|
|
||||||
|
public Picture(
|
||||||
|
@NonNull final String originalFilename,
|
||||||
|
@Nullable final ZonedDateTime originalDate,
|
||||||
|
final long size,
|
||||||
|
final int width,
|
||||||
|
final int height,
|
||||||
|
@Nullable final String make,
|
||||||
|
@Nullable final String model,
|
||||||
|
@NonNull final String uploadFilename,
|
||||||
|
@NonNull final ZonedDateTime uploadDate,
|
||||||
|
@NonNull final String uploadAddress,
|
||||||
|
@Nullable final String uploadAgent
|
||||||
|
) {
|
||||||
|
this.originalFilename = originalFilename;
|
||||||
|
this.originalDate = originalDate;
|
||||||
|
this.size = size;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.make = make;
|
||||||
|
this.model = model;
|
||||||
|
this.uploadFilename = uploadFilename;
|
||||||
|
this.uploadDate = uploadDate;
|
||||||
|
this.uploadAddress = uploadAddress;
|
||||||
|
this.uploadAgent = uploadAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package de.ph87.isabell_und_timo.picture;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@CrossOrigin
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("PictureAdmin")
|
||||||
|
public class PictureAdminController {
|
||||||
|
|
||||||
|
private final PictureService pictureService;
|
||||||
|
|
||||||
|
private final PictureRepository pictureRepository;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@GetMapping("visible")
|
||||||
|
public String visible(@NonNull final HttpServletRequest request) {
|
||||||
|
verifyLocal(request);
|
||||||
|
return pictureRepository
|
||||||
|
.findAllDtoByVisibleTrue()
|
||||||
|
.stream()
|
||||||
|
.map(p -> "<a href='/PictureAdmin/%s/visible/false'><img src='/Picture/%s/preview' loading='lazy' alt='%s' style='width: 100%%;'></a>\n".formatted(p.uuid, p.uuid, p.uuid))
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// @GetMapping("{uuid}/visible/{visible}")
|
||||||
|
// public void visible(@PathVariable @NonNull final String uuid, @PathVariable final boolean visible, @NonNull final HttpServletRequest request, @NonNull final HttpServletResponse response) throws IOException {
|
||||||
|
// verifyLocal(request);
|
||||||
|
// pictureService.visible(uuid, visible);
|
||||||
|
// response.sendRedirect(request.get);
|
||||||
|
// }
|
||||||
|
|
||||||
|
private static void verifyLocal(final HttpServletRequest request) {
|
||||||
|
if (!request.getRemoteAddr().startsWith("10.") && !request.getRemoteAddr().equals("127.0.0.1")) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,204 @@
|
|||||||
|
package de.ph87.isabell_und_timo.picture;
|
||||||
|
|
||||||
|
import com.drew.imaging.ImageMetadataReader;
|
||||||
|
import com.drew.imaging.ImageProcessingException;
|
||||||
|
import com.drew.metadata.Metadata;
|
||||||
|
import com.drew.metadata.MetadataException;
|
||||||
|
import com.drew.metadata.exif.ExifIFD0Directory;
|
||||||
|
import com.drew.metadata.exif.ExifSubIFDDirectory;
|
||||||
|
import com.mortennobel.imagescaling.DimensionConstrain;
|
||||||
|
import com.mortennobel.imagescaling.MultiStepRescaleOp;
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.servlet.MultipartConfigElement;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.web.servlet.MultipartConfigFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.util.unit.DataSize;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.image.AffineTransformOp;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.ph87.isabell_und_timo.picture.CONFIG.PREVIEW_SIZE;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@CrossOrigin
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("Picture")
|
||||||
|
public class PictureController {
|
||||||
|
|
||||||
|
private final PictureService pictureService;
|
||||||
|
|
||||||
|
private final PictureRepository pictureRepository;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MultipartConfigElement multipartConfigElement() {
|
||||||
|
final MultipartConfigFactory factory = new MultipartConfigFactory();
|
||||||
|
factory.setMaxFileSize(DataSize.ofMegabytes(15));
|
||||||
|
factory.setMaxRequestSize(DataSize.ofMegabytes(15));
|
||||||
|
return factory.createMultipartConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@GetMapping("all")
|
||||||
|
public List<PictureDto> all() {
|
||||||
|
return pictureRepository.findAllDtoByVisibleTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{uuid}/preview")
|
||||||
|
public void preview(@PathVariable @NonNull final String uuid, @NonNull final HttpServletRequest request, @NonNull final HttpServletResponse response) throws IOException {
|
||||||
|
final PictureInternal pictureDto = pictureRepository.findDtoByUuid(uuid).orElseThrow(() -> {
|
||||||
|
log.warn("Tried accessing NON-EXISTENT picture: {}", request);
|
||||||
|
return new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
});
|
||||||
|
if (!pictureDto.visible) {
|
||||||
|
log.warn("Tried accessing INVISIBLE picture: {}", request);
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
try (final FileInputStream input = new FileInputStream(pictureDto.getPreviewPath().toFile())) {
|
||||||
|
response.getOutputStream().write(input.readAllBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "upload")
|
||||||
|
public void uploadImage(
|
||||||
|
@NonNull @RequestParam("file") final MultipartFile fileStream,
|
||||||
|
@NonNull final HttpServletRequest request,
|
||||||
|
@Nullable @RequestHeader(value = "User-Agent", required = false) final String userAgent
|
||||||
|
) throws IOException, ImageProcessingException {
|
||||||
|
if (fileStream.isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing 'file'");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String originalFilename = fileStream.getOriginalFilename() == null ? "" : fileStream.getOriginalFilename();
|
||||||
|
final int dotIndex = originalFilename.lastIndexOf('.');
|
||||||
|
final String extension = (dotIndex >= 0) ? originalFilename.substring(dotIndex + 1) : "jpg";
|
||||||
|
final String baseName = (dotIndex >= 0) ? originalFilename.substring(0, dotIndex) : originalFilename;
|
||||||
|
final ZonedDateTime uploadDate = ZonedDateTime.now();
|
||||||
|
final String filename = "%s_%s.%s".formatted(uploadDate.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), baseName, extension);
|
||||||
|
|
||||||
|
final ByteArrayInputStream buffer = new ByteArrayInputStream(fileStream.getBytes());
|
||||||
|
BufferedImage originalImage = ImageIO.read(buffer);
|
||||||
|
if (originalImage == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Not a valid image");
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.reset();
|
||||||
|
final Metadata metadata = ImageMetadataReader.readMetadata(buffer);
|
||||||
|
final ExifSubIFDDirectory exif = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
||||||
|
final ZonedDateTime date = exif != null && exif.getDateOriginal() != null ? ZonedDateTime.ofInstant(exif.getDateOriginal().toInstant(), ZoneId.systemDefault()) : null;
|
||||||
|
final ExifIFD0Directory ifd0 = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
|
||||||
|
final String make = ifd0 != null ? ifd0.getString(ExifIFD0Directory.TAG_MAKE) : null;
|
||||||
|
final String model = ifd0 != null ? ifd0.getString(ExifIFD0Directory.TAG_MODEL) : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (ifd0 != null && ifd0.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
|
||||||
|
final int orientation = ifd0.getInt(ExifIFD0Directory.TAG_ORIENTATION);
|
||||||
|
originalImage = transformImageByOrientation(originalImage, orientation);
|
||||||
|
}
|
||||||
|
} catch (MetadataException e) {
|
||||||
|
log.error("Failed to read orientation: filename={}, error={}", filename, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
final PictureInternal pictureDto = pictureService.create(date, make, model, originalImage.getWidth(), originalImage.getHeight(), fileStream.getSize(), originalFilename, filename, uploadDate, request.getRemoteAddr(), userAgent);
|
||||||
|
final Path originalPath = pictureDto.getOriginalPath();
|
||||||
|
final Path previewPath = pictureDto.getPreviewPath();
|
||||||
|
|
||||||
|
mkdir(originalPath.getParent());
|
||||||
|
mkdir(previewPath.getParent());
|
||||||
|
|
||||||
|
buffer.reset();
|
||||||
|
Files.copy(buffer, originalPath);
|
||||||
|
|
||||||
|
final MultiStepRescaleOp rescale = new MultiStepRescaleOp(DimensionConstrain.createMaxDimension(PREVIEW_SIZE, PREVIEW_SIZE));
|
||||||
|
final BufferedImage previewImage = rescale.filter(originalImage, null);
|
||||||
|
ImageIO.write(previewImage, extension, previewPath.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mkdir(@NonNull final Path directory) {
|
||||||
|
if (directory.toFile().mkdirs()) {
|
||||||
|
log.info("Directory created: {}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BufferedImage transformImageByOrientation(BufferedImage image, int orientation) {
|
||||||
|
int w = image.getWidth();
|
||||||
|
int h = image.getHeight();
|
||||||
|
final AffineTransform tx = new AffineTransform();
|
||||||
|
|
||||||
|
switch (orientation) {
|
||||||
|
case 1:
|
||||||
|
return image;
|
||||||
|
case 2: // Spiegeln horizontal
|
||||||
|
tx.scale(-1, 1);
|
||||||
|
tx.translate(-w, 0);
|
||||||
|
break;
|
||||||
|
case 3: // 180°
|
||||||
|
tx.translate(w, h);
|
||||||
|
tx.rotate(Math.toRadians(180));
|
||||||
|
break;
|
||||||
|
case 4: // Spiegeln vertikal
|
||||||
|
tx.scale(1, -1);
|
||||||
|
tx.translate(0, -h);
|
||||||
|
break;
|
||||||
|
case 5: // 90° + Spiegelung horizontal
|
||||||
|
tx.translate(h, 0);
|
||||||
|
tx.rotate(Math.toRadians(90));
|
||||||
|
tx.scale(-1, 1);
|
||||||
|
break;
|
||||||
|
case 6: // 90°
|
||||||
|
tx.translate(h, 0);
|
||||||
|
tx.rotate(Math.toRadians(90));
|
||||||
|
break;
|
||||||
|
case 7: // 270° + Spiegelung horizontal
|
||||||
|
tx.translate(0, w);
|
||||||
|
tx.rotate(Math.toRadians(270));
|
||||||
|
tx.scale(-1, 1);
|
||||||
|
break;
|
||||||
|
case 8: // 270°
|
||||||
|
tx.translate(0, w);
|
||||||
|
tx.rotate(Math.toRadians(270));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Falls rotiert: Breite/Höhe ggf. tauschen
|
||||||
|
boolean swapDim = (orientation == 5 || orientation == 6 || orientation == 7 || orientation == 8);
|
||||||
|
int newW = swapDim ? h : w;
|
||||||
|
int newH = swapDim ? w : h;
|
||||||
|
|
||||||
|
// Transformation anwenden
|
||||||
|
final AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BICUBIC);
|
||||||
|
final BufferedImage transformed = new BufferedImage(newW, newH, image.getType());
|
||||||
|
op.filter(image, transformed);
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package de.ph87.isabell_und_timo.picture;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PictureDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String uuid;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final ZonedDateTime originalDate;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ZonedDateTime uploadDate;
|
||||||
|
|
||||||
|
public PictureDto(@NonNull final Picture picture) {
|
||||||
|
this.uuid = picture.getUuid();
|
||||||
|
this.originalDate = picture.getOriginalDate();
|
||||||
|
this.uploadDate = picture.getUploadDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package de.ph87.isabell_und_timo.picture;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PictureInternal {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
public final String uuid;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String originalFilename;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final ZonedDateTime originalDate;
|
||||||
|
|
||||||
|
public final long size;
|
||||||
|
|
||||||
|
public final int height;
|
||||||
|
|
||||||
|
public final int width;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final String make;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final String model;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String uploadFilename;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final ZonedDateTime uploadDate;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String uploadAddress;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final String uploadAgent;
|
||||||
|
|
||||||
|
public final boolean visible;
|
||||||
|
|
||||||
|
public PictureInternal(@NonNull final Picture picture) {
|
||||||
|
this.uuid = picture.getUuid();
|
||||||
|
this.originalFilename = picture.getOriginalFilename();
|
||||||
|
this.originalDate = picture.getOriginalDate();
|
||||||
|
this.size = picture.getSize();
|
||||||
|
this.height = picture.getHeight();
|
||||||
|
this.width = picture.getWidth();
|
||||||
|
this.make = picture.getMake();
|
||||||
|
this.model = picture.getModel();
|
||||||
|
this.uploadFilename = picture.getUploadFilename();
|
||||||
|
this.uploadDate = picture.getUploadDate();
|
||||||
|
this.uploadAddress = picture.getUploadAddress();
|
||||||
|
this.uploadAgent = picture.getUploadAgent();
|
||||||
|
this.visible = picture.isVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Path getOriginalPath() {
|
||||||
|
return CONFIG.ORIGINAL.resolve(uploadFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public Path getPreviewPath() {
|
||||||
|
return CONFIG.RESIZED.resolve(uploadFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package de.ph87.isabell_und_timo.picture;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface PictureRepository extends ListCrudRepository<Picture, String> {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Query("select new de.ph87.isabell_und_timo.picture.PictureDto(p) from Picture p where p.visible = true")
|
||||||
|
List<PictureDto> findAllDtoByVisibleTrue();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Query("select new de.ph87.isabell_und_timo.picture.PictureInternal(p) from Picture p where p.uuid = :uuid")
|
||||||
|
Optional<PictureInternal> findDtoByUuid(@NonNull String uuid);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package de.ph87.isabell_und_timo.picture;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PictureService {
|
||||||
|
|
||||||
|
private final PictureRepository pictureRepository;
|
||||||
|
|
||||||
|
private final ResourcePatternResolver resourcePatternResolver;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Transactional
|
||||||
|
public PictureInternal create(
|
||||||
|
@Nullable final ZonedDateTime date,
|
||||||
|
@Nullable final String make,
|
||||||
|
@Nullable final String model,
|
||||||
|
final int width,
|
||||||
|
final int height,
|
||||||
|
final long size,
|
||||||
|
@NonNull final String originalFilename,
|
||||||
|
@NonNull final String uploadFilename,
|
||||||
|
@NonNull final ZonedDateTime uploadDate,
|
||||||
|
@NonNull final String uploadAddress,
|
||||||
|
@Nullable final String uploadAgent
|
||||||
|
) {
|
||||||
|
final Picture picture = pictureRepository.save(new Picture(originalFilename, date, size, width, height, make, model, uploadFilename, uploadDate, uploadAddress, uploadAgent));
|
||||||
|
log.info("Uploaded: {}", picture);
|
||||||
|
return new PictureInternal(picture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void visible(final @NonNull String uuid, final boolean visible) {
|
||||||
|
final Picture picture = pictureRepository.findById(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
picture.setVisible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
src/main/resources/application.properties
Normal file
8
src/main/resources/application.properties
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
logging.level.root=WARN
|
||||||
|
logging.level.de.ph87=INFO
|
||||||
|
#-
|
||||||
|
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl
|
||||||
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
|
spring.jpa.open-in-view=false
|
||||||
|
#-
|
||||||
|
spring.main.banner-mode=off
|
||||||
Loading…
Reference in New Issue
Block a user