Basic implementation for notes app
This commit is contained in:
@@ -21,6 +21,10 @@
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>web-container-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||
<artifactId>files-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package de.nbscloud.notes.controller;
|
||||
|
||||
import de.nbscloud.files.api.FilesService;
|
||||
import de.nbscloud.notes.NotesApp;
|
||||
import de.nbscloud.webcontainer.MessageHelper;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
public class NotesController implements InitializingBean {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NotesController.class);
|
||||
|
||||
@Autowired
|
||||
private AppRegistry appRegistry;
|
||||
@Autowired
|
||||
private FilesService filesService;
|
||||
@Autowired
|
||||
private WebContainerSharedConfig webContainerSharedConfig;
|
||||
@Autowired
|
||||
private NotesApp app;
|
||||
@Autowired
|
||||
private MessageHelper messageHelper;
|
||||
|
||||
public static Optional<Path> notePathToPath(String notePath) {
|
||||
final Optional<String> optNotePath = Optional.ofNullable(notePath);
|
||||
|
||||
return optNotePath.map(p -> Path.of(p));
|
||||
}
|
||||
|
||||
@GetMapping("/notes")
|
||||
public String start(Model model, @RequestParam(required = false) String notePath) {
|
||||
final Optional<Path> optNotePath = notePathToPath(notePath);
|
||||
|
||||
if(optNotePath.isPresent()) {
|
||||
final byte[] content = this.filesService.get(app, optNotePath.get());
|
||||
|
||||
model.addAttribute("content", new String(content));
|
||||
}
|
||||
|
||||
this.messageHelper.addAndClearAll(model);
|
||||
model.addAttribute("currentNote", notePath);
|
||||
model.addAttribute("tree", this.filesService.getTree(app, Optional.empty()));
|
||||
model.addAttribute("dirs", this.filesService.collectDirs(app, Optional.empty()));
|
||||
model.addAttribute("apps", this.appRegistry.getAll());
|
||||
this.webContainerSharedConfig.addDefaults(model);
|
||||
|
||||
return "notes/notesIndex";
|
||||
}
|
||||
|
||||
@PostMapping("/notes/save")
|
||||
public String save(String textContent, String notePath) {
|
||||
final Optional<Path> optNotePath = notePathToPath(notePath);
|
||||
|
||||
if(optNotePath.isPresent()) {
|
||||
this.filesService.overwriteFile(app, optNotePath.get(), textContent.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.notes.save.success");
|
||||
}
|
||||
|
||||
return "redirect:?notePath=" + notePath;
|
||||
}
|
||||
|
||||
@PostMapping("/notes/delete")
|
||||
public String delete(String notePath) {
|
||||
final Optional<Path> optNotePath = notePathToPath(notePath);
|
||||
|
||||
if(optNotePath.isPresent()) {
|
||||
this.filesService.delete(app, optNotePath.get());
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.notes.delete.success");
|
||||
}
|
||||
|
||||
return "redirect:";
|
||||
}
|
||||
|
||||
@PostMapping("/notes/createDir")
|
||||
public String createDirectory(String dirName, String parentPath, String currentPath) {
|
||||
final Optional<Path> optParentPath = notePathToPath(parentPath);
|
||||
|
||||
if(optParentPath.isPresent()) {
|
||||
final Path dirPath = optParentPath.get().resolve(dirName);
|
||||
|
||||
this.filesService.createDirectory(app, dirPath);
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.notes.createDir.success");
|
||||
}
|
||||
|
||||
if(currentPath == null || currentPath.isBlank()) {
|
||||
return "redirect:";
|
||||
}
|
||||
|
||||
return "redirect:?notePath=" + currentPath;
|
||||
}
|
||||
|
||||
@PostMapping("/notes/createNote")
|
||||
public String createNote(String noteName, String parentPath, String currentPath) {
|
||||
final Optional<Path> optParentPath = notePathToPath(parentPath);
|
||||
|
||||
if(optParentPath.isPresent()) {
|
||||
final Path notePath = optParentPath.get().resolve(noteName);
|
||||
|
||||
this.filesService.createFile(app, notePath, new byte[] {});
|
||||
|
||||
this.messageHelper.addInfo("nbscloud.notes.createNote.success");
|
||||
|
||||
return "redirect:?notePath=" + notePath;
|
||||
}
|
||||
|
||||
return "redirect:?notePath=" + currentPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.filesService.createAppDirectory(this.app);
|
||||
}
|
||||
}
|
||||
7
notes/src/main/resources/i18n/notes_messages.properties
Normal file
7
notes/src/main/resources/i18n/notes_messages.properties
Normal file
@@ -0,0 +1,7 @@
|
||||
nbscloud.notes.action.save=Save
|
||||
nbscloud.notes.action.delete=Delete
|
||||
|
||||
nbscloud.notes.save.success=Saved
|
||||
nbscloud.notes.delete.success=Deleted
|
||||
nbscloud.notes.createDir.success=Directory created
|
||||
nbscloud.notes.createNote.success=Note created
|
||||
@@ -0,0 +1,7 @@
|
||||
nbscloud.notes.action.save=Speichern
|
||||
nbscloud.notes.action.delete=L\u00F6schen
|
||||
|
||||
nbscloud.notes.save.success=Gespeichert
|
||||
nbscloud.notes.delete.success=Gel\u00F6scht
|
||||
nbscloud.notes.createDir.success=Verzeichnis erstellt
|
||||
nbscloud.notes.createNote.success=Notiz erstellt
|
||||
59
notes/src/main/resources/static/css/notes_main.css
Normal file
59
notes/src/main/resources/static/css/notes_main.css
Normal file
@@ -0,0 +1,59 @@
|
||||
#content-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
row-gap: 2em;
|
||||
column-gap: 2em;
|
||||
}
|
||||
|
||||
#nav-tree {
|
||||
flex-grow: 1;
|
||||
background-color: var(--background-color-highlight);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#sub-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
row-gap: 2em;
|
||||
column-gap: 2em;
|
||||
flex-grow: 1;
|
||||
flex-basis: 70%;
|
||||
}
|
||||
|
||||
#nav-tree ul {
|
||||
list-style-type: none;
|
||||
padding-left: 1.6em;
|
||||
}
|
||||
|
||||
#nav-tree p, a {
|
||||
margin: 0em;
|
||||
}
|
||||
|
||||
#edit-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
}
|
||||
|
||||
#edit-area textarea {
|
||||
flex: 1;
|
||||
background-color: var(--background-color-highlight);
|
||||
color: var(--text-color);
|
||||
resize: none;
|
||||
}
|
||||
|
||||
#actions-sub {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-wrap: nowrap;
|
||||
row-gap: 2em;
|
||||
column-gap: 2em;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
#dir-container > * {
|
||||
display: inline;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div th:fragment="tree-level">
|
||||
<ul>
|
||||
<li th:each="tmpSubTree : ${tree}">
|
||||
<div th:if="${tmpSubTree.directory}" id="dir-container">
|
||||
<span class="icon"></span>
|
||||
<p th:text="${tmpSubTree.name}" />
|
||||
</div>
|
||||
<p th:if="${!tmpSubTree.directory}"><a th:href="@{/notes(notePath=${tmpSubTree.path})}" th:text="${tmpSubTree.name}" /></p>
|
||||
<th:block th:include="@{notes/fragments/treeLevel} :: tree-level" th:with="tree=${tmpSubTree.subTree}" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
47
notes/src/main/resources/templates/notes/includes/menu.html
Normal file
47
notes/src/main/resources/templates/notes/includes/menu.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<div id="menu-container" th:fragment="menu">
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="create-dir-container" class="menu-container">
|
||||
<details>
|
||||
<summary>
|
||||
<span id="create-dir-container-span-input" class="icon menu-icon"></span>
|
||||
</summary>
|
||||
<div class="menu-modal">
|
||||
<div class="menu-modal-content">
|
||||
<form method="POST" action="#" th:action="@{/notes/createDir}" enctype="multipart/form-data">
|
||||
<input id="create-dir-container-dir-name" type="text" name="dirName" />
|
||||
<select size="1" id="parentPath" name="parentPath">
|
||||
<option th:each="dir : ${dirs}"
|
||||
th:value="${dir}"
|
||||
th:text="${dir}" />
|
||||
</select>
|
||||
<input type="hidden" name="notePath" th:value="${currentNote}" class="display-none" />
|
||||
<input type="submit" value="Create"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="create-note-container" class="menu-container">
|
||||
<details>
|
||||
<summary>
|
||||
<span id="create-note-container-span-input" class="icon menu-icon"></span>
|
||||
<div class="create-note-container-details-modal-overlay"></div>
|
||||
</summary>
|
||||
<div class="menu-modal">
|
||||
<div class="menu-modal-content">
|
||||
<form method="POST" action="#" th:action="@{/notes/createNote}" enctype="multipart/form-data">
|
||||
<input id="create-note-container-dir-name" type="text" name="noteName" />
|
||||
<select size="1" id="parentPathNote" name="parentPath">
|
||||
<option th:each="dir : ${dirs}"
|
||||
th:value="${dir}"
|
||||
th:text="${dir}" />
|
||||
</select>
|
||||
<input type="hidden" name="notePath" th:value="${currentNote}" class="display-none" />
|
||||
<input type="submit" value="Create"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
40
notes/src/main/resources/templates/notes/notesIndex.html
Normal file
40
notes/src/main/resources/templates/notes/notesIndex.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title th:text="#{nbscloud.notes.index.title} + ${currentNote}"/>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}"/>
|
||||
<link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}"/>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}"/>
|
||||
<link rel="stylesheet" th:href="@{/css/notes_main.css}"/>
|
||||
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<div th:replace="includes/header :: header"/>
|
||||
<div th:replace="includes/messages :: messages"/>
|
||||
<div th:replace="notes/includes/menu :: menu"/>
|
||||
<div id="content-container">
|
||||
<div id="nav-tree">
|
||||
<th:block th:include="@{notes/fragments/treeLevel} :: tree-level" th:with="tree=${tree}" />
|
||||
</div>
|
||||
<div id="sub-content">
|
||||
<form method="POST" action="#" enctype="multipart/form-data">
|
||||
<div id="edit-area">
|
||||
<textarea type="text" th:if="${content != null}" th:text="${content}" rows="25" name="textContent"/>
|
||||
</div>
|
||||
<div id="actions">
|
||||
<div th:if="${content != null}" id="actions-sub">
|
||||
<input type="submit" th:value="#{nbscloud.notes.action.save}" th:formaction="@{/notes/save}" />
|
||||
<input type="submit" th:value="#{nbscloud.notes.action.delete}" th:formaction="@{/notes/delete}" />
|
||||
<input type="hidden" name="notePath" th:value="${currentNote}" class="display-none" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user