1
0
This commit is contained in:
2023-02-21 18:31:01 +01:00
parent 455cb63652
commit b6e6a94a1e
20 changed files with 495 additions and 21 deletions

View File

@@ -31,5 +31,9 @@
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>de.77zzcx7.nbs-cloud</groupId>
<artifactId>files-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,5 @@
package de.nbscloud.todo;
public enum Category {
A, B, C;
}

View File

@@ -1,19 +1,26 @@
package de.nbscloud.todo;
import de.nbscloud.todo.widget.TodoWidget;
import de.nbscloud.webcontainer.registry.App;
import de.nbscloud.webcontainer.registry.AppRegistry;
import de.nbscloud.webcontainer.registry.Widget;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
@Component
public class TodoApp implements App, InitializingBean {
public static final String ID = "todo";
@Autowired
private AppRegistry appRegistry;
@Override
public String getId() {
return "todo";
return ID;
}
@Override
@@ -31,6 +38,11 @@ public class TodoApp implements App, InitializingBean {
return 40;
}
@Override
public Collection<Widget> getWidgets() {
return List.of(new TodoWidget());
}
@Override
public void afterPropertiesSet() throws Exception {
this.appRegistry.registerApp(this);

View File

@@ -0,0 +1,74 @@
package de.nbscloud.todo;
import java.time.LocalDate;
import java.util.UUID;
public class TodoItem {
private String id;
private boolean done;
private String text;
private Category category;
private LocalDate added;
private LocalDate due;
public static final TodoItem create(boolean done, String text, Category category, LocalDate added, LocalDate due) {
final TodoItem item = new TodoItem();
item.setDone(done);
item.setText(text);
item.setCategory(category);
item.setAdded(added);
item.setDue(due);
item.setId(UUID.randomUUID().toString());
return item;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public LocalDate getAdded() {
return added;
}
public void setAdded(LocalDate added) {
this.added = added;
}
public LocalDate getDue() {
return due;
}
public void setDue(LocalDate due) {
this.due = due;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}

View File

@@ -0,0 +1,16 @@
package de.nbscloud.todo;
import java.util.ArrayList;
import java.util.List;
public class TodoList {
private List<TodoItem> todos = new ArrayList<>();
public List<TodoItem> getTodos() {
return todos;
}
public void setTodos(List<TodoItem> todos) {
this.todos = todos;
}
}

View File

@@ -0,0 +1,159 @@
package de.nbscloud.todo.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.nbscloud.files.api.FilesService;
import de.nbscloud.todo.Category;
import de.nbscloud.todo.TodoApp;
import de.nbscloud.todo.TodoItem;
import de.nbscloud.todo.TodoList;
import de.nbscloud.webcontainer.MessageHelper;
import de.nbscloud.webcontainer.registry.AppRegistry;
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
import de.nbscloud.webcontainer.shared.util.ControllerUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
@Controller
public class TodoController {
private static final Logger logger = LoggerFactory.getLogger(TodoController.class);
public static final Comparator<TodoItem> TODO_ITEM_COMPARATOR = Comparator.comparing(TodoItem::isDone)
.thenComparing(TodoItem::getDue,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(TodoItem::getAdded);
static final Path FILENAME = Path.of("todo");
@Autowired
private AppRegistry appRegistry;
@Autowired
private FilesService filesService;
@Autowired
private WebContainerSharedConfig webContainerSharedConfig;
@Autowired
private TodoApp app;
@Autowired
private MessageHelper messageHelper;
@Autowired
private ObjectMapper mapper;
@GetMapping("/todo")
public String start(Model model) {
try {
this.filesService.createAppDirectoryIfNotExists(this.app);
final String todoListJson = new String(this.filesService.get(app, FILENAME));
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
final Map<Category, List<TodoItem>> categoryMap = new TreeMap<>(Comparator.comparing(Category::name));
todos.getTodos().stream().collect(Collectors.groupingBy(TodoItem::getCategory, () -> categoryMap, Collectors.toList()));
if(categoryMap.containsKey(Category.A)) {
Collections.sort(categoryMap.get(Category.A), TODO_ITEM_COMPARATOR);
}
if(categoryMap.containsKey(Category.B)) {
Collections.sort(categoryMap.get(Category.B), TODO_ITEM_COMPARATOR);
}
if(categoryMap.containsKey(Category.C)) {
Collections.sort(categoryMap.get(Category.C), TODO_ITEM_COMPARATOR);
}
model.addAttribute("todosByCategory", categoryMap);
}
catch(JsonProcessingException e) {
logger.error("Error while parsing todo file", e);
this.messageHelper.addError(e.getMessage());
}
catch(RuntimeException re) {
if(re.getCause() instanceof NoSuchFileException) {
logger.debug("Todo file does not exist, create it");
try {
this.filesService.createFile(app, FILENAME, mapper.writeValueAsString(new TodoList()).getBytes(StandardCharsets.UTF_8));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
else {
logger.error("Error while reading todo file", re);
this.messageHelper.addError(re.getMessage());
}
}
model.addAttribute("categories", Category.values());
this.messageHelper.addAndClearAll(model);
model.addAttribute("apps", this.appRegistry.getAll());
this.webContainerSharedConfig.addDefaults(model);
return "todo/todoIndex";
}
@PostMapping("/todo/addItem")
public String addItem(Model model, String text, Category category, String due) {
try {
final String todoListJson = new String(this.filesService.get(app, FILENAME));
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
todos.getTodos().add(TodoItem.create(false, text, category, LocalDate.now(), ControllerUtils.parseDate(due)));
this.filesService.overwriteFile(app, FILENAME, mapper.writeValueAsBytes(todos));
this.messageHelper.addInfo("nbscloud.todo.item.add");
} catch (JsonProcessingException e) {
logger.error("Error", e);
this.messageHelper.addError(e.getMessage());
}
model.addAttribute("categories", Category.values());
model.addAttribute("apps", this.appRegistry.getAll());
this.webContainerSharedConfig.addDefaults(model);
return "redirect:";
}
@PostMapping("/todo/toggle")
public String toggle(Model model, String id, boolean done) {
try {
final String todoListJson = new String(this.filesService.get(app, FILENAME));
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
todos.getTodos().stream().filter(item -> item.getId().equals(id)).findFirst().ifPresent(item -> item.setDone(done));
this.filesService.overwriteFile(app, FILENAME, mapper.writeValueAsBytes(todos));
if(done) {
this.messageHelper.addInfo("nbscloud.todo.item.toggle.done");
}
else {
this.messageHelper.addInfo("nbscloud.todo.item.toggle.inprogress");
}
} catch (JsonProcessingException e) {
logger.error("Error", e);
this.messageHelper.addError(e.getMessage());
}
model.addAttribute("categories", Category.values());
model.addAttribute("apps", this.appRegistry.getAll());
this.webContainerSharedConfig.addDefaults(model);
return "redirect:";
}
}

View File

@@ -0,0 +1,58 @@
package de.nbscloud.todo.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.nbscloud.files.api.FilesService;
import de.nbscloud.todo.Category;
import de.nbscloud.todo.TodoApp;
import de.nbscloud.todo.TodoItem;
import de.nbscloud.todo.TodoList;
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.stream.Collectors;
@Controller
public class TodoWidgetController {
private static final Logger logger = LoggerFactory.getLogger(TodoWidgetController.class);
@Autowired
private WebContainerSharedConfig webContainerSharedConfig;
@Autowired
private FilesService filesService;
@Autowired
private TodoApp app;
@Autowired
private ObjectMapper mapper;
@GetMapping("todo/widgets/todoOverview")
public String getTodoOverview(HttpServletRequest request, HttpServletResponse response, Model model) {
try {
this.filesService.createAppDirectoryIfNotExists(this.app);
final String todoListJson = new String(this.filesService.get(app, TodoController.FILENAME));
final TodoList todos = mapper.readValue(todoListJson, TodoList.class);
final List<TodoItem> topFiveTodos = todos.getTodos().stream().filter(item -> !item.isDone()).sorted(
Comparator.comparing(TodoItem::getDue,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(TodoItem::getCategory)
.thenComparing(TodoItem::getAdded)).limit(5).collect(Collectors.toList());
model.addAttribute("todos", topFiveTodos);
}
catch(JsonProcessingException e) {
logger.error("Error while parsing todo file", e);
}
return "todo/widgets/todoOverview :: todo-overview";
}
}

View File

@@ -0,0 +1,11 @@
package de.nbscloud.todo.widget;
import de.nbscloud.todo.TodoApp;
import de.nbscloud.webcontainer.registry.Widget;
public class TodoWidget implements Widget {
@Override
public String getPath() {
return TodoApp.ID + "/widgets/todoOverview";
}
}

View File

@@ -0,0 +1,7 @@
nbscloud.todo.index.title=nbscloud - todo
nbscloud.todo.widget.heading=todo overview
nbscloud.todo.item.add=Todo added
nbscloud.todo.item.toggle.done=Done
nbscloud.todo.item.toggle.inprogress=In progress

View File

@@ -0,0 +1,7 @@
nbscloud.todo.index.title=nbscloud - todo
nbscloud.todo.widget.heading=todo \u00FCbersicht
nbscloud.todo.item.add=Todo hinzugef\u00FCgt
nbscloud.todo.item.toggle.done=Fertig
nbscloud.todo.item.toggle.inprogress=Unfertig

View File

@@ -0,0 +1,24 @@
#content-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
row-gap: 2em;
column-gap: 2em;
}
.todo-category {
flex-grow: 1;
background-color:var(--background-color-highlight);
padding: 1em;
align-self: stretch;
word-break: break-all;
flex-basis: 100%;
}
.todo-category-header {
text-decoration-line: underline;
}
.todo-item > td:nth-child(2) {
padding-inline: 3em;
}

View File

@@ -0,0 +1,24 @@
<div id="menu-container" th:fragment="menu">
<div class="menu-spacer"></div>
<div id="add-item-container" class="menu-container">
<details>
<summary>
<span id="add-item-container-span-input" class="icon menu-icon">&#xf23a;</span>
</summary>
<div class="menu-modal">
<div class="menu-modal-content">
<form method="POST" action="#" th:action="@{/todo/addItem}" enctype="multipart/form-data">
<input id="add-item-container-text" type="text" name="text" />
<select size="1" id="add-item-category" name="category">
<option th:each="category : ${categories}"
th:value="${category}"
th:text="${category}" />
</select>
<input type="date" id="add-item-due" name="due"/>
<input type="submit" value="Add"/>
</form>
</div>
</div>
</details>
</div>
</div>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{nbscloud.todo.index.title}"/>
<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/todo_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="todo/includes/menu :: menu"/>
<div id="content-container">
<div class="todo-category" th:each="entry : ${todosByCategory}">
<p th:text="${#strings.prepend(#strings.append(entry.key, #strings.repeat('&nbsp;', 10)), '&nbsp;')}"
class="todo-category-header"/>
<table id="todo-item-table">
<tr class="todo-item" th:each="todo : ${entry.value}">
<td>
<form method="POST" th:action="@{/todo/toggle}" enctype="multipart/form-data">
<input type="checkbox" th:checked="${todo.done}" name="done" onClick="this.form.submit()" /> <!-- fucking javascript -->
<input type="hidden" name="id" th:value="${todo.id}" class="display-none"/>
</form>
</td>
<td th:text="${todo.text}"/>
<td th:text="${todo.due}"/>
</tr>
</table>
</div>
</div>
<div th:replace="includes/footer :: footer"/>
</div>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<div id="todo-widget" class="widget" th:fragment="todo-overview">
<p class="widget-heading" th:text="#{nbscloud.todo.widget.heading}"/>
<table id="todo-widget-table">
<tbody>
<tr th:each="todo : ${todos}">
<td th:text="${todo.category}"/>
<td th:text="${todo.text}"/>
<td th:text="${todo.due}"/>
</tr>
</tbody>
</table>
</div>