Several features:
- Ability to serve static files, HTML and CSS are supported so far - Consequently, show a simple HTML form to enter the parameters when navigating to / (or /index.html). - Make the server multi-threaded (in a rather primitive way so far). - Use logback instead of the simple-slf4j logger.
This commit is contained in:
		
							parent
							
								
									cd29c6fe75
								
							
						
					
					
						commit
						4d0c5f5c18
					
				
					 7 changed files with 327 additions and 94 deletions
				
			
		|  | @ -13,15 +13,16 @@ | ||||||
|     <orderEntry type="inheritedJdk" /> |     <orderEntry type="inheritedJdk" /> | ||||||
|     <orderEntry type="sourceFolder" forTests="false" /> |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|     <orderEntry type="module" module-name="labyrinth-generator" /> |     <orderEntry type="module" module-name="labyrinth-generator" /> | ||||||
|     <orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.20" level="project" /> |     <orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.25" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.20" level="project" /> |     <orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.25" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" /> |     <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" /> |     <orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" /> |     <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" /> | ||||||
|     <orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.12" level="project" /> |     <orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.12" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" /> |     <orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" /> |     <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.35" level="project" /> | ||||||
|     <orderEntry type="library" name="Maven: org.slf4j:slf4j-simple:1.7.30" level="project" /> |     <orderEntry type="library" name="Maven: ch.qos.logback:logback-classic:1.2.10" level="project" /> | ||||||
|  |     <orderEntry type="library" name="Maven: ch.qos.logback:logback-core:1.2.10" level="project" /> | ||||||
|     <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" /> |     <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" /> | ||||||
|     <orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" /> |     <orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" /> | ||||||
|     <orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" /> |     <orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" /> | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										28
									
								
								pom.xml
									
										
									
									
									
								
							|  | @ -14,6 +14,10 @@ | ||||||
| 	<artifactId>labyrinth-server</artifactId> | 	<artifactId>labyrinth-server</artifactId> | ||||||
| 	<version>0.0.1-SNAPSHOT</version> | 	<version>0.0.1-SNAPSHOT</version> | ||||||
| 
 | 
 | ||||||
|  | 	<properties> | ||||||
|  | 		<slf4j.version>1.7.35</slf4j.version> | ||||||
|  | 	</properties> | ||||||
|  | 
 | ||||||
| 	<dependencies> | 	<dependencies> | ||||||
| 		<dependency> | 		<dependency> | ||||||
| 			<groupId>ch.fritteli.labyrinth</groupId> | 			<groupId>ch.fritteli.labyrinth</groupId> | ||||||
|  | @ -35,12 +39,12 @@ | ||||||
| 		<dependency> | 		<dependency> | ||||||
| 			<groupId>org.slf4j</groupId> | 			<groupId>org.slf4j</groupId> | ||||||
| 			<artifactId>slf4j-api</artifactId> | 			<artifactId>slf4j-api</artifactId> | ||||||
| 			<version>1.7.30</version> | 			<version>${slf4j.version}</version> | ||||||
| 		</dependency> | 		</dependency> | ||||||
| 		<dependency> | 		<dependency> | ||||||
| 			<groupId>org.slf4j</groupId> | 			<groupId>ch.qos.logback</groupId> | ||||||
| 			<artifactId>slf4j-simple</artifactId> | 			<artifactId>logback-classic</artifactId> | ||||||
| 			<version>1.7.30</version> | 			<version>1.2.10</version> | ||||||
| 		</dependency> | 		</dependency> | ||||||
| 		<dependency> | 		<dependency> | ||||||
| 			<groupId>org.junit.jupiter</groupId> | 			<groupId>org.junit.jupiter</groupId> | ||||||
|  | @ -95,12 +99,24 @@ | ||||||
| 	</distributionManagement> | 	</distributionManagement> | ||||||
| 	<repositories> | 	<repositories> | ||||||
| 		<repository> | 		<repository> | ||||||
| 			<id>repo.gittr.ch</id> | 			<id>repo.gittr.ch.releases</id> | ||||||
| 			<url>https://repo.gittr.ch/</url> | 			<url>https://repo.gittr.ch/releases/</url> | ||||||
| 			<releases> | 			<releases> | ||||||
| 				<enabled>true</enabled> | 				<enabled>true</enabled> | ||||||
| 				<updatePolicy>never</updatePolicy> | 				<updatePolicy>never</updatePolicy> | ||||||
| 			</releases> | 			</releases> | ||||||
|  | 			<snapshots> | ||||||
|  | 				<enabled>false</enabled> | ||||||
|  | 				<updatePolicy>never</updatePolicy> | ||||||
|  | 			</snapshots> | ||||||
|  | 		</repository> | ||||||
|  | 		<repository> | ||||||
|  | 			<id>repo.gittr.ch.snapshots</id> | ||||||
|  | 			<url>https://repo.gittr.ch/snapshots/</url> | ||||||
|  | 			<releases> | ||||||
|  | 				<enabled>false</enabled> | ||||||
|  | 				<updatePolicy>never</updatePolicy> | ||||||
|  | 			</releases> | ||||||
| 			<snapshots> | 			<snapshots> | ||||||
| 				<enabled>true</enabled> | 				<enabled>true</enabled> | ||||||
| 				<updatePolicy>always</updatePolicy> | 				<updatePolicy>always</updatePolicy> | ||||||
|  |  | ||||||
|  | @ -24,20 +24,24 @@ import org.jetbrains.annotations.Nullable; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.OutputStream; | import java.io.OutputStream; | ||||||
| import java.net.InetSocketAddress; | import java.net.InetSocketAddress; | ||||||
|  | import java.net.URISyntaxException; | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.util.concurrent.ExecutorService; | ||||||
|  | import java.util.concurrent.Executors; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
| import java.util.concurrent.atomic.AtomicBoolean; | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
| 
 | 
 | ||||||
| @Slf4j | @Slf4j | ||||||
| public class LabyrinthServer { | public class LabyrinthServer { | ||||||
|  |     @NonNull | ||||||
|     private final HttpServer httpServer; |     private final HttpServer httpServer; | ||||||
|  |     @NonNull | ||||||
|  |     private final ExecutorService executorService = Executors.newCachedThreadPool(); | ||||||
| 
 | 
 | ||||||
|     public LabyrinthServer(@NonNull final ServerConfig config) throws IOException { |     public LabyrinthServer(@NonNull final ServerConfig config) throws IOException, URISyntaxException { | ||||||
|         this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5); |         this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5); | ||||||
|         this.httpServer.createContext("/", exchange -> { |         this.httpServer.createContext("/", new StaticResourcesFileHandler(this.executorService)); | ||||||
|             exchange.getResponseHeaders().add("Location", "/create"); |  | ||||||
|             exchange.sendResponseHeaders(302, -1); |  | ||||||
|         }); |  | ||||||
|         this.httpServer.createContext("/create", this::handleCreate); |         this.httpServer.createContext("/create", this::handleCreate); | ||||||
|         this.httpServer.createContext("/render", this::handleRender); |         this.httpServer.createContext("/render", this::handleRender); | ||||||
|     } |     } | ||||||
|  | @ -94,16 +98,25 @@ public class LabyrinthServer { | ||||||
|     public void stop() { |     public void stop() { | ||||||
|         log.info("Stopping server ..."); |         log.info("Stopping server ..."); | ||||||
|         this.httpServer.stop(5); |         this.httpServer.stop(5); | ||||||
|  |         this.executorService.shutdown(); | ||||||
|  |         try { | ||||||
|  |             if (!this.executorService.awaitTermination(5, TimeUnit.SECONDS)) { | ||||||
|  |                 log.warn("Timeout occurred while awaiting termination of executor service"); | ||||||
|  |             } | ||||||
|  |         } catch (final InterruptedException e) { | ||||||
|  |             log.error("Failed to await termination of executor service", e); | ||||||
|  |         } | ||||||
|         log.info("Server stopped."); |         log.info("Server stopped."); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void handleCreate(HttpExchange exchange) throws IOException { |     private void handleCreate(HttpExchange exchange) throws IOException { | ||||||
|  |         this.executorService.submit(() -> { | ||||||
|             log.debug("Handling request to {}", exchange.getRequestURI()); |             log.debug("Handling request to {}", exchange.getRequestURI()); | ||||||
|  |             try { | ||||||
|                 final String requestMethod = exchange.getRequestMethod(); |                 final String requestMethod = exchange.getRequestMethod(); | ||||||
|                 if (!requestMethod.equals("GET")) { |                 if (!requestMethod.equals("GET")) { | ||||||
|                     exchange.getResponseBody().close(); |                     exchange.getResponseBody().close(); | ||||||
|                     exchange.sendResponseHeaders(405, -1); |                     exchange.sendResponseHeaders(405, -1); | ||||||
|             exchange.close(); |  | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI().getQuery()); |                 final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI().getQuery()); | ||||||
|  | @ -118,7 +131,6 @@ public class LabyrinthServer { | ||||||
|                 if (needsRedirect.get()) { |                 if (needsRedirect.get()) { | ||||||
|                     responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id); |                     responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id); | ||||||
|                     exchange.sendResponseHeaders(302, -1); |                     exchange.sendResponseHeaders(302, -1); | ||||||
|             exchange.close(); |  | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 final Labyrinth labyrinth = new Labyrinth(width, height, id); |                 final Labyrinth labyrinth = new Labyrinth(width, height, id); | ||||||
|  | @ -131,18 +143,26 @@ public class LabyrinthServer { | ||||||
|                     final OutputStream responseBody = exchange.getResponseBody(); |                     final OutputStream responseBody = exchange.getResponseBody(); | ||||||
|                     responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8)); |                     responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8)); | ||||||
|                     responseBody.flush(); |                     responseBody.flush(); | ||||||
|             exchange.close(); |  | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 responseHeaders.add("Content-type", output.getContentType()); |                 responseHeaders.add("Content-type", output.getContentType()); | ||||||
|  |                 if (output.equals(OutputType.BINARY)) { | ||||||
|  |                     responseHeaders.add("Content-disposition", "attachment; filename=\"labyrinth-" + width + "x" + height + "-" + id + ".laby\""); | ||||||
|  |                 } | ||||||
|                 exchange.sendResponseHeaders(200, 0); |                 exchange.sendResponseHeaders(200, 0); | ||||||
|                 final OutputStream responseBody = exchange.getResponseBody(); |                 final OutputStream responseBody = exchange.getResponseBody(); | ||||||
|                 responseBody.write(render); |                 responseBody.write(render); | ||||||
|                 responseBody.flush(); |                 responseBody.flush(); | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 log.error("FSCK!", e); | ||||||
|  |             } finally { | ||||||
|                 exchange.close(); |                 exchange.close(); | ||||||
|             } |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     private void handleRender(final HttpExchange exchange) throws IOException { |     private void handleRender(final HttpExchange exchange) throws IOException { | ||||||
|  |         this.executorService.submit(() -> { | ||||||
|             try { |             try { | ||||||
|                 log.debug("Handling request to {}", exchange.getRequestURI()); |                 log.debug("Handling request to {}", exchange.getRequestURI()); | ||||||
|                 final String requestMethod = exchange.getRequestMethod(); |                 final String requestMethod = exchange.getRequestMethod(); | ||||||
|  | @ -152,15 +172,10 @@ public class LabyrinthServer { | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 final byte[] bytes = exchange.getRequestBody().readAllBytes(); |                 final byte[] bytes = exchange.getRequestBody().readAllBytes(); | ||||||
|             @NonNull final Labyrinth labyrinth; |  | ||||||
|             try { |  | ||||||
|                 labyrinth = SerializerDeserializer.deserialize(bytes); |  | ||||||
|             } catch (Exception e) { |  | ||||||
|                 e.printStackTrace(); |  | ||||||
|                 throw e; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             OutputType output = exchange.getRequestHeaders() |                 final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes); | ||||||
|  | 
 | ||||||
|  |                 final OutputType output = exchange.getRequestHeaders() | ||||||
|                         .get("Accept") |                         .get("Accept") | ||||||
|                         .contains("text/html") ? |                         .contains("text/html") ? | ||||||
|                         OutputType.HTML : |                         OutputType.HTML : | ||||||
|  | @ -182,9 +197,12 @@ public class LabyrinthServer { | ||||||
|                 final OutputStream responseBody = exchange.getResponseBody(); |                 final OutputStream responseBody = exchange.getResponseBody(); | ||||||
|                 responseBody.write(render); |                 responseBody.write(render); | ||||||
|                 responseBody.flush(); |                 responseBody.flush(); | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 log.error("FSCK!", e); | ||||||
|             } finally { |             } finally { | ||||||
|                 exchange.close(); |                 exchange.close(); | ||||||
|             } |             } | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private enum RequestParameter { |     private enum RequestParameter { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,98 @@ | ||||||
|  | package ch.fritteli.labyrinth.server; | ||||||
|  | 
 | ||||||
|  | import com.sun.net.httpserver.HttpExchange; | ||||||
|  | import com.sun.net.httpserver.HttpHandler; | ||||||
|  | import lombok.NonNull; | ||||||
|  | import lombok.extern.slf4j.Slf4j; | ||||||
|  | import org.apache.pdfbox.io.IOUtils; | ||||||
|  | import org.jetbrains.annotations.Nullable; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.net.URI; | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.util.concurrent.ExecutorService; | ||||||
|  | 
 | ||||||
|  | @Slf4j | ||||||
|  | public class StaticResourcesFileHandler implements HttpHandler { | ||||||
|  | 
 | ||||||
|  |     public static final String WEBASSETS_DIRECTORY = "webassets"; | ||||||
|  |     private final ExecutorService executorService; | ||||||
|  | 
 | ||||||
|  |     public StaticResourcesFileHandler(final ExecutorService executorService) { | ||||||
|  |         this.executorService = executorService; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws IOException { | ||||||
|  |         log.debug("Sending redirect to {}", target); | ||||||
|  |         exchange.getResponseHeaders().add("Location", target); | ||||||
|  |         exchange.sendResponseHeaders(302, -1); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void notFound(@NonNull final HttpExchange exchange, @NonNull final String path) throws IOException { | ||||||
|  |         log.debug("Resource '{}' not found, replying with HTTP 404", path); | ||||||
|  |         exchange.getResponseHeaders().add("Content-type", "text/plain; charset=utf-8"); | ||||||
|  |         exchange.sendResponseHeaders(404, 0); | ||||||
|  |         exchange.getResponseBody().write("404 - Not found".getBytes(StandardCharsets.UTF_8)); | ||||||
|  |         exchange.getResponseBody().flush(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private static byte[] getBytes(@NonNull final String path) throws IOException { | ||||||
|  |         final InputStream stream = StaticResourcesFileHandler.class.getClassLoader().getResourceAsStream(WEBASSETS_DIRECTORY + path); | ||||||
|  |         if (stream == null) { | ||||||
|  |             log.debug("Resource '{}' not found in classpath.", path); | ||||||
|  |             return new byte[0]; | ||||||
|  |         } | ||||||
|  |         return IOUtils.toByteArray(stream); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     private static String getMimeType(@NonNull final String path) { | ||||||
|  |         if (path.endsWith(".html")) { | ||||||
|  |             return "text/html"; | ||||||
|  |         } | ||||||
|  |         if (path.endsWith(".css")) { | ||||||
|  |             return "text/css"; | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void handle(@NonNull final HttpExchange exchange) throws IOException { | ||||||
|  |         this.executorService.submit(() -> { | ||||||
|  |             try { | ||||||
|  |                 final URI requestURI = exchange.getRequestURI(); | ||||||
|  |                 final String path = requestURI.getPath(); | ||||||
|  |                 log.debug("Handling request to {}", path); | ||||||
|  |                 if ("/".equals(path)) { | ||||||
|  |                     redirect(exchange, "/index.html"); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if (!path.startsWith("/")) { | ||||||
|  |                     notFound(exchange, path); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final String mimeType = getMimeType(path); | ||||||
|  |                 if (mimeType == null) { | ||||||
|  |                     notFound(exchange, path); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final byte[] responseBytes = getBytes(path); | ||||||
|  |                 if (responseBytes.length == 0) { | ||||||
|  |                     notFound(exchange, path); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 log.debug("Serving {}{} with mimetype {}: {} bytes", WEBASSETS_DIRECTORY, path, mimeType, responseBytes.length); | ||||||
|  |                 exchange.getResponseHeaders().add("Content-type", mimeType); | ||||||
|  |                 exchange.sendResponseHeaders(200, 0); | ||||||
|  |                 exchange.getResponseBody().write(responseBytes); | ||||||
|  |                 exchange.getResponseBody().flush(); | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 log.error("FSCK!", e); | ||||||
|  |             } finally { | ||||||
|  |                 exchange.close(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								src/main/resources/logback.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/main/resources/logback.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8" ?> | ||||||
|  | <configuration> | ||||||
|  | 	<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/> | ||||||
|  | 
 | ||||||
|  | 	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | ||||||
|  | 		<!-- encoders are by default assigned the type | ||||||
|  | 		ch.qos.logback.classic.encoder.PatternLayoutEncoder --> | ||||||
|  | 		<encoder> | ||||||
|  | 			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | ||||||
|  | 		</encoder> | ||||||
|  | 	</appender> | ||||||
|  | 
 | ||||||
|  | 	<root level="info"> | ||||||
|  | 		<appender-ref ref="STDOUT"/> | ||||||
|  | 	</root> | ||||||
|  | 	<logger name="ch.fritteli.labyrinth.server.StaticResourcesFileHandler" level="debug"/> | ||||||
|  | </configuration> | ||||||
							
								
								
									
										32
									
								
								src/main/resources/webassets/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/main/resources/webassets/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |     <meta charset="UTF-8"> | ||||||
|  |     <title>Labyrinth Generator</title> | ||||||
|  |     <link rel="stylesheet" href="style.css"> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | <div class="content"> | ||||||
|  |     <h1>Labyrinth Generator</h1> | ||||||
|  |     <p>Enter some values, click the "Create!" button and see what happens!</p> | ||||||
|  |     <form action="/create" method="get"> | ||||||
|  |         <div class="inputs-wrapper"> | ||||||
|  |             <label for="width">Width:</label><input id="width" name="width" type="number" min="1" required> | ||||||
|  |             <label for="height">Height:</label><input id="height" name="height" type="number" min="1" required> | ||||||
|  |             <label for="output">Output format:</label> | ||||||
|  |             <select id="output" name="output" required> | ||||||
|  |                 <option label="HTML Document" value="html"></option> | ||||||
|  |                 <option label="Plain text" value="text"></option> | ||||||
|  |                 <option label="PDF Document" value="pdf"></option> | ||||||
|  |                 <option label="Binary" value="binary"></option> | ||||||
|  |             </select> | ||||||
|  |             <label for="id">Seed (optional):</label><input id="id" name="id" type="number"> | ||||||
|  |         </div> | ||||||
|  |         <div class="controls-wrapper"> | ||||||
|  |             <button type="submit" class="primary">Create!</button> | ||||||
|  |             <button type="reset">Reset form</button> | ||||||
|  |         </div> | ||||||
|  |     </form> | ||||||
|  | </div> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										51
									
								
								src/main/resources/webassets/style.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/main/resources/webassets/style.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | :root { | ||||||
|  |     --color-background: #181a1b; | ||||||
|  |     --color-foreground: #e8e6e3; | ||||||
|  |     --color-border: #5d6164; | ||||||
|  |     --color-background-highlight: #292b2c; | ||||||
|  |     --color-foreground-highlight: #f8f7f4; | ||||||
|  |     --color-border-highlight: #6e7275; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | body { | ||||||
|  |     display: flex; | ||||||
|  |     font-family: sans-serif; | ||||||
|  |     justify-content: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | body, button, input, select { | ||||||
|  |     background-color: var(--color-background); | ||||||
|  |     color: var(--color-foreground); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | button, input, select { | ||||||
|  |     border: 1px solid var(--color-border); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | button:active, input:active, select:active, | ||||||
|  | button:focus, input:focus, select:focus, | ||||||
|  | button:hover, input:hover, select:hover { | ||||||
|  |     background-color: var(--color-background-highlight); | ||||||
|  |     border-color: var(--color-border-highlight); | ||||||
|  |     color: var(--color-foreground-highlight); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | button.primary { | ||||||
|  |     background-color: #ffcc00; | ||||||
|  |     border-color: #eebb00; | ||||||
|  |     color: var(--color-background); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .content { | ||||||
|  |     width: 75%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .content h1 { | ||||||
|  |     text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .inputs-wrapper { | ||||||
|  |     display: grid; | ||||||
|  |     grid-row-gap: 1em; | ||||||
|  |     grid-template-columns: 0.5fr 1fr; | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue