Basic implementation for notes app
This commit is contained in:
@@ -1,7 +1,3 @@
|
|||||||
#content-container {
|
|
||||||
padding-top: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#widgets-container {
|
#widgets-container {
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -16,7 +12,7 @@
|
|||||||
|
|
||||||
.widget {
|
.widget {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
background-color: #1d2121;
|
background-color:var(--background-color-highlight);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import de.nbscloud.webcontainer.registry.App;
|
|||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -12,22 +13,80 @@ public interface FilesService {
|
|||||||
LocalDateTime lastModified) {
|
LocalDateTime lastModified) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createAppDirectory(App app);
|
class ContentTree {
|
||||||
|
private String name;
|
||||||
|
private boolean directory;
|
||||||
|
private String path;
|
||||||
|
private List<ContentTree> subTree = new ArrayList<>();
|
||||||
|
private ContentTree parent;
|
||||||
|
|
||||||
public void createDirectory(App app, Path path);
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
public void createFile(App app, Path path, byte[] content);
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
public void delete(App app, Path path);
|
public boolean isDirectory() {
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
public void get(App app, Path path);
|
public void setDirectory(boolean directory) {
|
||||||
|
this.directory = directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPath(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ContentTree> getSubTree() {
|
||||||
|
return subTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubTree(List<ContentTree> subTree) {
|
||||||
|
this.subTree = subTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getHasSubTree() {
|
||||||
|
return !this.subTree.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentTree getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParent(ContentTree parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void createAppDirectory(App app);
|
||||||
|
|
||||||
|
void createDirectory(App app, Path path);
|
||||||
|
|
||||||
|
void createFile(App app, Path path, byte[] content);
|
||||||
|
|
||||||
|
void overwriteFile(App app, Path path, byte[] content);
|
||||||
|
|
||||||
|
void delete(App app, Path path);
|
||||||
|
|
||||||
|
byte[] get(App app, Path path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paths in return list are always relative to the appDir.
|
* Paths in return list are always relative to the appDir. Non-recursive list.
|
||||||
*
|
*
|
||||||
* @param app to list the files for
|
* @param app to list the files for
|
||||||
* @param path in case of {@link Optional#EMPTY} the appDir is used as start dir. If not empty, path has to be relative
|
* @param path in case of {@link Optional#EMPTY} the appDir is used as start dir. If not empty, path has to be relative
|
||||||
* to the appDir
|
* to the appDir
|
||||||
*/
|
*/
|
||||||
public List<ContentContainer> list(App app, Optional<Path> path);
|
List<ContentContainer> list(App app, Optional<Path> path);
|
||||||
|
|
||||||
|
ContentTree getTree(App app, Optional<Path> path);
|
||||||
|
|
||||||
|
List<String> collectDirs(App app, Optional<Path> path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.nbscloud.files;
|
package de.nbscloud.files;
|
||||||
|
|
||||||
import de.nbscloud.files.api.FilesService.ContentContainer;
|
import de.nbscloud.files.api.FilesService.ContentContainer;
|
||||||
|
import de.nbscloud.files.api.FilesService.ContentTree;
|
||||||
import de.nbscloud.files.config.FilesConfig;
|
import de.nbscloud.files.config.FilesConfig;
|
||||||
import de.nbscloud.files.exception.FileSystemServiceException;
|
import de.nbscloud.files.exception.FileSystemServiceException;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
@@ -21,6 +22,7 @@ import java.time.ZoneId;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
@@ -117,6 +119,14 @@ public class FileSystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void overwriteFile(Path path, byte[] content) {
|
||||||
|
try {
|
||||||
|
Files.write(path, content, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileSystemServiceException("Could not overwrite file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Path move(Path originalPath, Path newPath) {
|
public Path move(Path originalPath, Path newPath) {
|
||||||
try {
|
try {
|
||||||
return Files.move(originalPath, newPath, StandardCopyOption.ATOMIC_MOVE);
|
return Files.move(originalPath, newPath, StandardCopyOption.ATOMIC_MOVE);
|
||||||
@@ -286,7 +296,7 @@ public class FileSystemService {
|
|||||||
|
|
||||||
List<ContentContainer> list(Path startPath, SortOrder sortOrder, Function<Path, Path> relativizer) {
|
List<ContentContainer> list(Path startPath, SortOrder sortOrder, Function<Path, Path> relativizer) {
|
||||||
try {
|
try {
|
||||||
List<ContentContainer> contentList = Files.list(this.locationTracker.getCurrentLocation())
|
List<ContentContainer> contentList = Files.list(startPath)
|
||||||
.filter(path -> {
|
.filter(path -> {
|
||||||
boolean ok = Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.isDirectory(path);
|
boolean ok = Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.isDirectory(path);
|
||||||
|
|
||||||
@@ -319,22 +329,86 @@ public class FileSystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContentTree getTree(Path startPath, SortOrder sortOrder, Function<Path, Path> relativizer) {
|
||||||
|
try {
|
||||||
|
if (!Files.isDirectory(startPath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ContentTree root = new ContentTree();
|
||||||
|
|
||||||
|
root.setName(startPath.getFileName().toString());
|
||||||
|
root.setPath(relativizer.apply(startPath).toString());
|
||||||
|
root.setDirectory(true);
|
||||||
|
|
||||||
|
final AtomicReference<ContentTree> current = new AtomicReference<>(root);
|
||||||
|
|
||||||
|
Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
|
||||||
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||||
|
if (startPath.equals(dir)) {
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ContentTree dirTree = new ContentTree();
|
||||||
|
|
||||||
|
dirTree.setName(dir.getFileName().toString());
|
||||||
|
dirTree.setPath(relativizer.apply(dir).toString());
|
||||||
|
dirTree.setDirectory(true);
|
||||||
|
dirTree.setParent(current.get());
|
||||||
|
|
||||||
|
current.get().getSubTree().add(dirTree);
|
||||||
|
current.set(dirTree);
|
||||||
|
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||||
|
final ContentTree fileTree = new ContentTree();
|
||||||
|
|
||||||
|
fileTree.setName(file.getFileName().toString());
|
||||||
|
fileTree.setPath(relativizer.apply(file).toString());
|
||||||
|
fileTree.setDirectory(false);
|
||||||
|
fileTree.setParent(current.get());
|
||||||
|
|
||||||
|
current.get().getSubTree().add(fileTree);
|
||||||
|
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||||
|
current.set(current.get().getParent());
|
||||||
|
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return root;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FileSystemServiceException("Could not list files", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<ContentContainer> list(SortOrder sortOrder) {
|
public List<ContentContainer> list(SortOrder sortOrder) {
|
||||||
return list(this.locationTracker.getCurrentLocation(), sortOrder, path -> this.locationTracker.getRelativeToBaseDir(path));
|
return list(this.locationTracker.getCurrentLocation(), sortOrder, path -> this.locationTracker.getRelativeToBaseDir(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> collectDirs(String sourceFile) {
|
public List<String> collectDirs(String sourceFile) {
|
||||||
|
return collectDirs(this.locationTracker.resolve(sourceFile), this.locationTracker.getBaseDirPath(), path -> this.locationTracker.getRelativeToBaseDir(path), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> collectDirs(Path sourcePath, Path baseDir, Function<Path, Path> relativizer, boolean includeSource) {
|
||||||
try {
|
try {
|
||||||
final List<String> resultList = new ArrayList<>();
|
final List<String> resultList = new ArrayList<>();
|
||||||
final Path sourcePath = this.locationTracker.resolve(sourceFile);
|
|
||||||
final boolean sourceIsDir = Files.isDirectory(sourcePath);
|
final boolean sourceIsDir = Files.isDirectory(sourcePath);
|
||||||
|
|
||||||
Files.walkFileTree(this.locationTracker.getBaseDirPath(), new SimpleFileVisitor<Path>() {
|
Files.walkFileTree(baseDir, new SimpleFileVisitor<Path>() {
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||||
super.visitFile(dir, attrs);
|
super.visitFile(dir, attrs);
|
||||||
|
|
||||||
if (sourceIsDir && sourcePath.equals(dir)) {
|
if (!includeSource && sourceIsDir && sourcePath.equals(dir)) {
|
||||||
return FileVisitResult.SKIP_SUBTREE;
|
return FileVisitResult.SKIP_SUBTREE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +424,7 @@ public class FileSystemService {
|
|||||||
return FileVisitResult.SKIP_SUBTREE;
|
return FileVisitResult.SKIP_SUBTREE;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String relPath = locationTracker.getRelativeToBaseDir(dir).toString();
|
final String relPath = relativizer.apply(dir).toString();
|
||||||
|
|
||||||
resultList.add("/" + relPath);
|
resultList.add("/" + relPath);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ public class FilesServiceImpl implements FilesService {
|
|||||||
private AppLocationTracker locationTracker;
|
private AppLocationTracker locationTracker;
|
||||||
|
|
||||||
private Path resolve(App app, Path path) {
|
private Path resolve(App app, Path path) {
|
||||||
|
if(path.startsWith("/")) {
|
||||||
|
path = path.subpath(0, path.getNameCount());
|
||||||
|
}
|
||||||
|
|
||||||
return this.locationTracker.resolve(app.getId(), path);
|
return this.locationTracker.resolve(app.getId(), path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,14 +45,19 @@ public class FilesServiceImpl implements FilesService {
|
|||||||
this.fileSystemService.createFile(resolve(app, path), content);
|
this.fileSystemService.createFile(resolve(app, path), content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void overwriteFile(App app, Path path, byte[] content) {
|
||||||
|
this.fileSystemService.overwriteFile(resolve(app, path), content);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(App app, Path path) {
|
public void delete(App app, Path path) {
|
||||||
this.fileSystemService.delete(resolve(app, path));
|
this.fileSystemService.delete(resolve(app, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void get(App app, Path path) {
|
public byte[] get(App app, Path path) {
|
||||||
this.fileSystemService.get(resolve(app, path));
|
return this.fileSystemService.get(resolve(app, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -58,4 +67,21 @@ public class FilesServiceImpl implements FilesService {
|
|||||||
|
|
||||||
return this.fileSystemService.list(p, FileSystemService.SortOrder.NATURAL, callbackPath -> appPath.relativize(callbackPath));
|
return this.fileSystemService.list(p, FileSystemService.SortOrder.NATURAL, callbackPath -> appPath.relativize(callbackPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContentTree getTree(App app, Optional<Path> path) {
|
||||||
|
final Path appPath = resolve(app, Path.of(""));
|
||||||
|
final Path p = path.map(tmpPath -> resolve(app, tmpPath)).orElse(appPath);
|
||||||
|
|
||||||
|
return this.fileSystemService.getTree(p, FileSystemService.SortOrder.NATURAL, callbackPath -> appPath.relativize(callbackPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> collectDirs(App app, Optional<Path> path) {
|
||||||
|
final Path appPath = resolve(app, Path.of(""));
|
||||||
|
final Path p = path.map(tmpPath -> resolve(app, tmpPath)).orElse(appPath);
|
||||||
|
|
||||||
|
|
||||||
|
return this.fileSystemService.collectDirs(p, appPath, callbackPath -> appPath.relativize(callbackPath), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import de.nbscloud.files.config.FilesConfig;
|
|||||||
import de.nbscloud.files.exception.FileSystemServiceException;
|
import de.nbscloud.files.exception.FileSystemServiceException;
|
||||||
import de.nbscloud.files.form.RenameForm;
|
import de.nbscloud.files.form.RenameForm;
|
||||||
import de.nbscloud.files.form.ShareForm;
|
import de.nbscloud.files.form.ShareForm;
|
||||||
|
import de.nbscloud.webcontainer.MessageHelper;
|
||||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||||
import org.apache.commons.io.input.ObservableInputStream;
|
import org.apache.commons.io.input.ObservableInputStream;
|
||||||
@@ -64,12 +65,8 @@ public class FilesController implements InitializingBean {
|
|||||||
private AppRegistry appRegistry;
|
private AppRegistry appRegistry;
|
||||||
@Autowired
|
@Autowired
|
||||||
private ObjectMapper mapper;
|
private ObjectMapper mapper;
|
||||||
|
@Autowired
|
||||||
// We have to temporarily store messages as we redirect: in the manipulation methods
|
private MessageHelper messageHelper;
|
||||||
// so everything we add to the model will be gone
|
|
||||||
private final List<String> errors = new ArrayList<>();
|
|
||||||
private final List<String> shareInfo = new ArrayList<>();
|
|
||||||
private final List<String> infoMessages = new ArrayList<>();
|
|
||||||
|
|
||||||
@GetMapping("/files/browse/**")
|
@GetMapping("/files/browse/**")
|
||||||
public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
|
public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
|
||||||
@@ -77,9 +74,7 @@ public class FilesController implements InitializingBean {
|
|||||||
|
|
||||||
updateLocation(httpServletRequest);
|
updateLocation(httpServletRequest);
|
||||||
|
|
||||||
model.addAttribute("errors", getAndClear(this.errors));
|
this.messageHelper.addAndClearAll(model);
|
||||||
model.addAttribute("infoMessages", getAndClear(this.infoMessages));
|
|
||||||
model.addAttribute("shareInfo", getAndClear(this.shareInfo));
|
|
||||||
model.addAttribute("currentLocation", getCurrentLocationPrefixed());
|
model.addAttribute("currentLocation", getCurrentLocationPrefixed());
|
||||||
model.addAttribute("content", getContent(order));
|
model.addAttribute("content", getContent(order));
|
||||||
model.addAttribute("sortOrder", order);
|
model.addAttribute("sortOrder", order);
|
||||||
@@ -98,17 +93,17 @@ public class FilesController implements InitializingBean {
|
|||||||
this.locationTracker.resolve(filename),
|
this.locationTracker.resolve(filename),
|
||||||
this.locationTracker.resolveTrash(filename));
|
this.locationTracker.resolveTrash(filename));
|
||||||
|
|
||||||
this.infoMessages.add("nbscloud.files.delete.success");
|
this.messageHelper.addInfo("nbscloud.files.delete.success");
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
logger.error("Could not soft delete file", e);
|
logger.error("Could not soft delete file", e);
|
||||||
|
|
||||||
this.errors.add(e.getMessage());
|
this.messageHelper.addError(e.getMessage());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Hard delete
|
// Hard delete
|
||||||
this.fileSystemService.delete(filename);
|
this.fileSystemService.delete(filename);
|
||||||
|
|
||||||
this.infoMessages.add("nbscloud.files.delete.success");
|
this.messageHelper.addInfo("nbscloud.files.delete.success");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||||
@@ -138,11 +133,11 @@ public class FilesController implements InitializingBean {
|
|||||||
try {
|
try {
|
||||||
this.fileSystemService.move(sourcePath, targetPath);
|
this.fileSystemService.move(sourcePath, targetPath);
|
||||||
|
|
||||||
this.infoMessages.add("nbscloud.files.rename.success");
|
this.messageHelper.addInfo("nbscloud.files.rename.success");
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
logger.error("Could not rename file", e);
|
logger.error("Could not rename file", e);
|
||||||
|
|
||||||
this.errors.add(e.getMessage());
|
this.messageHelper.addError(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||||
@@ -194,11 +189,11 @@ public class FilesController implements InitializingBean {
|
|||||||
try {
|
try {
|
||||||
this.fileSystemService.createFile(file.getOriginalFilename(), file.getBytes());
|
this.fileSystemService.createFile(file.getOriginalFilename(), file.getBytes());
|
||||||
|
|
||||||
this.infoMessages.add("nbscloud.files.created.file.success");
|
this.messageHelper.addInfo("nbscloud.files.created.file.success");
|
||||||
} catch (IOException | RuntimeException e) {
|
} catch (IOException | RuntimeException e) {
|
||||||
logger.error("Could not upload file", e);
|
logger.error("Could not upload file", e);
|
||||||
|
|
||||||
this.errors.add(e.getMessage());
|
this.messageHelper.addError(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,11 +205,11 @@ public class FilesController implements InitializingBean {
|
|||||||
try {
|
try {
|
||||||
this.fileSystemService.createDirectory(dirName);
|
this.fileSystemService.createDirectory(dirName);
|
||||||
|
|
||||||
this.infoMessages.add("nbscloud.files.created.dir.success");
|
this.messageHelper.addInfo("nbscloud.files.created.dir.success");
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
logger.error("Could not create dir", e);
|
logger.error("Could not create dir", e);
|
||||||
|
|
||||||
this.errors.add(e.getMessage());
|
this.messageHelper.addError(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||||
@@ -241,6 +236,7 @@ public class FilesController implements InitializingBean {
|
|||||||
final Share share = new Share();
|
final Share share = new Share();
|
||||||
// The format is always "yyyy-MM-dd", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date
|
// The format is always "yyyy-MM-dd", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date
|
||||||
final LocalDate expiryDate = Optional.ofNullable(expiryDateString)
|
final LocalDate expiryDate = Optional.ofNullable(expiryDateString)
|
||||||
|
.map(ed -> ed.isBlank() ? null : ed)
|
||||||
.map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
|
.map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
@@ -254,11 +250,11 @@ public class FilesController implements InitializingBean {
|
|||||||
|
|
||||||
this.fileSystemService.createFile(this.locationTracker.resolveShare(share.getUuid()), shareJson.getBytes(StandardCharsets.UTF_8));
|
this.fileSystemService.createFile(this.locationTracker.resolveShare(share.getUuid()), shareJson.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
this.shareInfo.add("/files/shares?shareUuid=" + share.getUuid());
|
this.messageHelper.addShare("/files/shares?shareUuid=" + share.getUuid());
|
||||||
} catch (RuntimeException | JsonProcessingException e) {
|
} catch (RuntimeException | JsonProcessingException e) {
|
||||||
logger.error("Could not share file", e);
|
logger.error("Could not share file", e);
|
||||||
|
|
||||||
this.errors.add(e.getMessage());
|
this.messageHelper.addError(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||||
@@ -325,20 +321,12 @@ public class FilesController implements InitializingBean {
|
|||||||
} catch (FileSystemServiceException e) {
|
} catch (FileSystemServiceException e) {
|
||||||
logger.error("Could not generate gallery", e);
|
logger.error("Could not generate gallery", e);
|
||||||
|
|
||||||
this.errors.add(e.getMessage());
|
this.messageHelper.addError(e.getMessage());
|
||||||
|
|
||||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getAndClear(List<String> source) {
|
|
||||||
final List<String> retList = new ArrayList<>(source);
|
|
||||||
|
|
||||||
source.clear();
|
|
||||||
|
|
||||||
return retList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ContentContainer> getContent(FileSystemService.SortOrder order) {
|
private List<ContentContainer> getContent(FileSystemService.SortOrder order) {
|
||||||
final List<ContentContainer> contentList = this.fileSystemService.list(order);
|
final List<ContentContainer> contentList = this.fileSystemService.list(order);
|
||||||
|
|
||||||
|
|||||||
@@ -73,105 +73,6 @@
|
|||||||
width: 70em;
|
width: 70em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu-container {
|
|
||||||
padding-top: 2em;
|
|
||||||
margin-left: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-menu-icon {
|
|
||||||
background: none !important;
|
|
||||||
border: none;
|
|
||||||
padding: 0 !important;
|
|
||||||
color: var(--text-color);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.files-menu-icon:hover {
|
|
||||||
color: var(--link-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-container {
|
|
||||||
padding-top: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#create-dir-container > details > summary {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#create-dir-container > details[open] > summary {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu-container > div {
|
|
||||||
display: inline-block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-dir-container-details-modal-content {
|
|
||||||
padding: 0.5em;
|
|
||||||
pointer-events: all;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-dir-container-details-modal {
|
|
||||||
background: var(--background-color);
|
|
||||||
filter: brightness(var(--background-brightness));
|
|
||||||
border-radius: 0.3em;
|
|
||||||
left: auto;
|
|
||||||
top: auto;
|
|
||||||
right: auto;
|
|
||||||
bottom: auto;
|
|
||||||
padding: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-spacer {
|
|
||||||
width: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageContainer {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-top: 2em;
|
|
||||||
border: 1px;
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
padding: 1em;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shareMessage {
|
|
||||||
border-color: var(--good-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoMessage {
|
|
||||||
border-color: var(--good-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorMessage {
|
|
||||||
border-color: var(--error-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 450px) {
|
|
||||||
#menu-container {
|
|
||||||
padding-top: 1em;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-spacer {
|
|
||||||
width: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#gallery-link {
|
#gallery-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,22 +13,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="main-container">
|
<div id="main-container">
|
||||||
<div th:replace="includes/header :: header"/>
|
<div th:replace="includes/header :: header"/>
|
||||||
<div class="messageContainer" th:each="shareInfo : ${shareInfo}">
|
<div th:replace="includes/messages :: messages"/>
|
||||||
<div th:if="${!shareInfo.isEmpty()}" class="shareMessage message">
|
|
||||||
<span th:text="#{'nbscloud.files.share-message'}"/>
|
|
||||||
<a th:href="@{${shareInfo}}" th:text="${shareInfo}"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="messageContainer" th:each="error : ${errors}">
|
|
||||||
<div th:if="${!errors.isEmpty()}" class="errorMessage message">
|
|
||||||
<span th:text="${error}"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="messageContainer" th:each="infoMessage : ${infoMessages}">
|
|
||||||
<div th:if="${!infoMessages.isEmpty()}" class="infoMessage message">
|
|
||||||
<span th:text="#{${infoMessage}}"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div th:replace="files/includes/menu :: menu"/>
|
<div th:replace="files/includes/menu :: menu"/>
|
||||||
<div id="content-container">
|
<div id="content-container">
|
||||||
<table id="files-content-table">
|
<table id="files-content-table">
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<div id="menu-container" th:fragment="menu">
|
<div id="menu-container" th:fragment="menu">
|
||||||
<div class="menu-spacer"></div>
|
<div class="menu-spacer"></div>
|
||||||
<div id="upload-container">
|
<div id="upload-container" class="menu-container">
|
||||||
<form method="POST" action="#" th:action="@{/files/upload}" enctype="multipart/form-data">
|
<form method="POST" action="#" th:action="@{/files/upload}" enctype="multipart/form-data">
|
||||||
<!-- JavaScript required unfortunately -->
|
<!-- JavaScript required unfortunately -->
|
||||||
<input id="upload-container-file-input" type="file" name="files" onchange="this.form.submit()"
|
<input id="upload-container-file-input" type="file" name="files" onchange="this.form.submit()"
|
||||||
style="display: none;" multiple>
|
style="display: none;" multiple>
|
||||||
<span id="upload-container-span-input" class="icon files-menu-icon"
|
<span id="upload-container-span-input" class="icon menu-icon"
|
||||||
onclick="document.getElementById('upload-container-file-input').click();"></span>
|
onclick="document.getElementById('upload-container-file-input').click();"></span>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-spacer"></div>
|
<div class="menu-spacer"></div>
|
||||||
<div id="create-dir-container">
|
<div id="create-dir-container" class="menu-container">
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
<span id="create-dir-container-span-input" class="icon files-menu-icon"></span>
|
<span id="create-dir-container-span-input" class="icon menu-icon"></span>
|
||||||
<div class="create-dir-container-details-modal-overlay"></div>
|
<div class="create-dir-container-details-modal-overlay"></div>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="create-dir-container-details-modal">
|
<div class="menu-modal">
|
||||||
<div class="create-dir-container-details-modal-content">
|
<div class="menu-modal-content">
|
||||||
<form method="POST" action="#" th:action="@{/files/createDir}" enctype="multipart/form-data">
|
<form method="POST" action="#" th:action="@{/files/createDir}" enctype="multipart/form-data">
|
||||||
<input id="create-dir-container-dir-name" type="text" name="dirName" />
|
<input id="create-dir-container-dir-name" type="text" name="dirName" />
|
||||||
<input type="submit" value="Create"/>
|
<input type="submit" value="Create"/>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="menu-spacer"></div>
|
<div class="menu-spacer"></div>
|
||||||
<div>
|
<div>
|
||||||
<a id="gallery-link" th:href="@{/files/gallery}">
|
<a id="gallery-link" th:href="@{/files/gallery}">
|
||||||
<span class="icon files-menu-icon"></span>
|
<span class="icon menu-icon"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,6 +21,10 @@
|
|||||||
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||||
<artifactId>web-container-config</artifactId>
|
<artifactId>web-container-config</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.77zzcx7.nbs-cloud</groupId>
|
||||||
|
<artifactId>files-api</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
<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>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package de.nbscloud.webcontainer;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class MessageHelper {
|
||||||
|
// We have to temporarily store messages as we redirect: in some methods
|
||||||
|
// so everything we add to the model will be gone, that's why we store messages
|
||||||
|
// temporarily in here
|
||||||
|
private final List<String> errors = new ArrayList<>();
|
||||||
|
private final List<String> shareInfo = new ArrayList<>();
|
||||||
|
private final List<String> infoMessages = new ArrayList<>();
|
||||||
|
|
||||||
|
public void addAndClearAll(Model model) {
|
||||||
|
model.addAttribute("errors", getAndClear(this.errors));
|
||||||
|
model.addAttribute("infoMessages", getAndClear(this.infoMessages));
|
||||||
|
model.addAttribute("shareInfo", getAndClear(this.shareInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> getAndClear(List<String> source) {
|
||||||
|
final List<String> retList = new ArrayList<>(source);
|
||||||
|
|
||||||
|
source.clear();
|
||||||
|
|
||||||
|
return retList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addError(String message) {
|
||||||
|
this.errors.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addShare(String message) {
|
||||||
|
this.shareInfo.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addInfo(String message) {
|
||||||
|
this.infoMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ info.build.group=@project.groupId@
|
|||||||
info.build.artifact=@project.artifactId@
|
info.build.artifact=@project.artifactId@
|
||||||
info.build.version=@project.version@
|
info.build.version=@project.version@
|
||||||
|
|
||||||
spring.messages.basename=i18n/container_messages,i18n/files_messages,i18n/dashboard_messages
|
spring.messages.basename=i18n/container_messages,i18n/files_messages,i18n/dashboard_messages,i18n/notes_messages
|
||||||
|
|
||||||
spring.servlet.multipart.max-file-size=-1
|
spring.servlet.multipart.max-file-size=-1
|
||||||
spring.servlet.multipart.max-request-size=-1
|
spring.servlet.multipart.max-request-size=-1
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
--error-color: #D30000;
|
--error-color: #D30000;
|
||||||
--text-color: #7f7f7f;
|
--text-color: #7f7f7f;
|
||||||
--background-color: #1d1f21;
|
--background-color: #1d1f21;
|
||||||
|
--background-color-highlight: #1d2121;
|
||||||
--link-color: #87ab63;
|
--link-color: #87ab63;
|
||||||
--hover-color: #1f1f2f;
|
--hover-color: #1f1f2f;
|
||||||
--border-color: #7f7f7f;
|
--border-color: #7f7f7f;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
--error-color: #D30000;
|
--error-color: #D30000;
|
||||||
--text-color: #000000;
|
--text-color: #000000;
|
||||||
--background-color: #FFFFFF;
|
--background-color: #FFFFFF;
|
||||||
|
--background-color-highlight: darkgrey;
|
||||||
--link-color: #0000EE;
|
--link-color: #0000EE;
|
||||||
--hover-color: lightgrey;
|
--hover-color: lightgrey;
|
||||||
--border-color: #ddd;
|
--border-color: #ddd;
|
||||||
|
|||||||
@@ -93,3 +93,106 @@ tr:hover {
|
|||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu-container {
|
||||||
|
padding-top: 2em;
|
||||||
|
margin-left: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
background: none !important;
|
||||||
|
border: none;
|
||||||
|
padding: 0 !important;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container > details > summary {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container > details[open] > summary {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon:hover {
|
||||||
|
color: var(--link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-spacer {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 450px) {
|
||||||
|
#menu-container {
|
||||||
|
padding-top: 1em;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-spacer {
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu-container > div {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-container {
|
||||||
|
padding-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-none {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageContainer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 2em;
|
||||||
|
border: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
padding: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareMessage {
|
||||||
|
border-color: var(--good-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoMessage {
|
||||||
|
border-color: var(--good-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMessage {
|
||||||
|
border-color: var(--error-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-modal-content {
|
||||||
|
padding: 0.5em;
|
||||||
|
pointer-events: all;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-modal {
|
||||||
|
background: var(--background-color);
|
||||||
|
filter: brightness(var(--background-brightness));
|
||||||
|
border-radius: 0.3em;
|
||||||
|
left: auto;
|
||||||
|
top: auto;
|
||||||
|
right: auto;
|
||||||
|
bottom: auto;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<div id="messages-container" th:fragment="messages">
|
||||||
|
<div class="messageContainer" th:each="shareInfo : ${shareInfo}">
|
||||||
|
<div th:if="${!shareInfo.isEmpty()}" class="shareMessage message">
|
||||||
|
<span th:text="#{'nbscloud.files.share-message'}"/>
|
||||||
|
<a th:href="@{${shareInfo}}" th:text="${shareInfo}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="messageContainer" th:each="error : ${errors}">
|
||||||
|
<div th:if="${!errors.isEmpty()}" class="errorMessage message">
|
||||||
|
<span th:text="${error}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="messageContainer" th:each="infoMessage : ${infoMessages}">
|
||||||
|
<div th:if="${!infoMessages.isEmpty()}" class="infoMessage message">
|
||||||
|
<span th:text="#{${infoMessage}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user