Major overhaul of navigation. Now shows directories and navigation buttons.
This commit is contained in:
parent
79a499ceec
commit
9d2fb0ea6e
14 changed files with 206 additions and 64 deletions
2
photos/portraits/album.properties
Normal file
2
photos/portraits/album.properties
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
cover=valdemar-dahl.jpg
|
||||||
2
pom.xml
2
pom.xml
|
|
@ -74,7 +74,7 @@
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
<version>4.4</version>
|
<version>4.4</version>
|
||||||
<scope>test</scope>
|
<!-- <scope>test</scope> -->
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework</groupId>
|
<groupId>org.springframework</groupId>
|
||||||
|
|
|
||||||
84
src/org/forkalsrud/album/exif/Album.java
Normal file
84
src/org/forkalsrud/album/exif/Album.java
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package org.forkalsrud.album.exif;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author knut
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class Album {
|
||||||
|
|
||||||
|
Entry cover;
|
||||||
|
|
||||||
|
List<Entry> contents = new ArrayList<Entry>();
|
||||||
|
|
||||||
|
public Entry getCover() {
|
||||||
|
return cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<Entry> getContents() {
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setCover(Entry cover) {
|
||||||
|
this.cover = cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void addContents(Entry entry) {
|
||||||
|
contents.add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void sort() {
|
||||||
|
|
||||||
|
Collections.sort(contents, new Comparator<Entry>() {
|
||||||
|
|
||||||
|
public int compare(Entry e1, Entry e2) {
|
||||||
|
|
||||||
|
if (!e1.isFile() && e2.isFile()) {
|
||||||
|
return -1;
|
||||||
|
} else if (e1.isFile() && !e2.isFile()) {
|
||||||
|
return +1;
|
||||||
|
}
|
||||||
|
Date d1 = e1.getDate();
|
||||||
|
Date d2 = e2.getDate();
|
||||||
|
if (d1 != null && d2 != null) {
|
||||||
|
return d1.compareTo(d2);
|
||||||
|
} else if (d1 != null) {
|
||||||
|
return -1;
|
||||||
|
} else if (d2 != null) {
|
||||||
|
return +1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fillLinkedList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void fillLinkedList() {
|
||||||
|
|
||||||
|
Entry prev = null;
|
||||||
|
for (Entry e : contents) {
|
||||||
|
e.prev = prev;
|
||||||
|
if (prev != null) {
|
||||||
|
prev.next = e;
|
||||||
|
}
|
||||||
|
prev = e;
|
||||||
|
}
|
||||||
|
if (prev != null) {
|
||||||
|
prev.next = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,8 +15,10 @@ import java.util.Date;
|
||||||
*/
|
*/
|
||||||
public class Entry {
|
public class Entry {
|
||||||
|
|
||||||
String name;
|
boolean isFile;
|
||||||
Dimension size;
|
String name;
|
||||||
|
String path;
|
||||||
|
Dimension size;
|
||||||
String caption;
|
String caption;
|
||||||
Date date;
|
Date date;
|
||||||
int orientation;
|
int orientation;
|
||||||
|
|
@ -25,6 +27,17 @@ public class Entry {
|
||||||
String etag;
|
String etag;
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isFile() {
|
||||||
|
return isFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setFile(boolean isFile) {
|
||||||
|
this.isFile = isFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Returns the name.
|
* @return Returns the name.
|
||||||
*/
|
*/
|
||||||
|
|
@ -132,4 +145,15 @@ public class Entry {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setPath(String path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ import java.text.NumberFormat;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
|
@ -54,7 +52,7 @@ public class EntryDao {
|
||||||
return directory.lastModified() <= new File(directory, CACHE_FILE).lastModified();
|
return directory.lastModified() <= new File(directory, CACHE_FILE).lastModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Entry> read(File directory) throws FileNotFoundException, IOException, JpegProcessingException, MetadataException, ParseException {
|
public Album read(File directory) throws FileNotFoundException, IOException, JpegProcessingException, MetadataException, ParseException {
|
||||||
|
|
||||||
List<Entry> entries = new ArrayList<Entry>();
|
List<Entry> entries = new ArrayList<Entry>();
|
||||||
|
|
||||||
|
|
@ -73,46 +71,25 @@ public class EntryDao {
|
||||||
Properties combined = new Properties();
|
Properties combined = new Properties();
|
||||||
combined.putAll(cachedProps);
|
combined.putAll(cachedProps);
|
||||||
combined.putAll(overrideProps);
|
combined.putAll(overrideProps);
|
||||||
populate(combined, entries);
|
Entry cover = populate(combined, entries);
|
||||||
Collections.sort(entries, new Comparator<Entry>() {
|
|
||||||
|
|
||||||
public int compare(Entry e1, Entry e2) {
|
Album alb = new Album();
|
||||||
|
alb.setCover(cover);
|
||||||
Date d1 = e1.getDate();
|
|
||||||
Date d2 = e2.getDate();
|
|
||||||
if (d1 != null && d2 != null) {
|
|
||||||
return d1.compareTo(d2);
|
|
||||||
} else if (d1 != null) {
|
|
||||||
return -1;
|
|
||||||
} else if (d2 != null) {
|
|
||||||
return +1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fillLinkedList(entries);
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
void fillLinkedList(List<Entry> entries) {
|
|
||||||
|
|
||||||
Entry prev = null;
|
|
||||||
for (Entry e : entries) {
|
for (Entry e : entries) {
|
||||||
e.prev = prev;
|
alb.addContents(e);
|
||||||
if (prev != null) {
|
|
||||||
prev.next = e;
|
|
||||||
}
|
|
||||||
prev = e;
|
|
||||||
}
|
|
||||||
if (prev != null) {
|
|
||||||
prev.next = null;
|
|
||||||
}
|
}
|
||||||
|
generateDirectoryEntries(directory, alb);
|
||||||
|
alb.sort();
|
||||||
|
return alb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void populate(Properties cachedProps, List<Entry> entries) throws ParseException {
|
|
||||||
|
|
||||||
|
|
||||||
|
private Entry populate(Properties cachedProps, List<Entry> entries) throws ParseException {
|
||||||
|
|
||||||
|
Entry cover = null;
|
||||||
|
String coverFileName = cachedProps.getProperty("cover");
|
||||||
HashMap<String, Entry> entryMap = new HashMap<String, Entry>();
|
HashMap<String, Entry> entryMap = new HashMap<String, Entry>();
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd-HHmmss");
|
||||||
Iterator i = cachedProps.keySet().iterator();
|
Iterator i = cachedProps.keySet().iterator();
|
||||||
|
|
@ -122,22 +99,28 @@ public class EntryDao {
|
||||||
String name = key.substring("file.".length(), key.length() - ".dimensions".length());
|
String name = key.substring("file.".length(), key.length() - ".dimensions".length());
|
||||||
if (!entryMap.containsKey(name)) {
|
if (!entryMap.containsKey(name)) {
|
||||||
Entry entry = new Entry();
|
Entry entry = new Entry();
|
||||||
|
entry.setFile(true);
|
||||||
entry.setName(name);
|
entry.setName(name);
|
||||||
|
entry.setPath(name);
|
||||||
entry.setDate(sdf.parse(cachedProps.getProperty("file." + name + ".captureDate")));
|
entry.setDate(sdf.parse(cachedProps.getProperty("file." + name + ".captureDate")));
|
||||||
entry.setSize(new Dimension(cachedProps.getProperty("file." + name + ".dimensions")));
|
entry.setSize(new Dimension(cachedProps.getProperty("file." + name + ".dimensions")));
|
||||||
entry.setCaption(cachedProps.getProperty("file." + name + ".caption"));
|
entry.setCaption(cachedProps.getProperty("file." + name + ".caption"));
|
||||||
entry.setOrientation(Integer.parseInt(cachedProps.getProperty("file." + name + ".orientation")));
|
entry.setOrientation(Integer.parseInt(cachedProps.getProperty("file." + name + ".orientation")));
|
||||||
entry.setEtag(cachedProps.getProperty("file." + name + ".etag"));
|
entry.setEtag(cachedProps.getProperty("file." + name + ".etag"));
|
||||||
entries.add(entry);
|
entries.add(entry);
|
||||||
|
if (name != null && name.equals(coverFileName)) {
|
||||||
|
cover = entry;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Entry readFile(File file) throws FileNotFoundException, IOException, JpegProcessingException, MetadataException, ParseException {
|
public Entry readFile(File file) throws FileNotFoundException, IOException, JpegProcessingException, MetadataException, ParseException {
|
||||||
|
|
||||||
List<Entry> dir = read(file.getParentFile());
|
List<Entry> dir = read(file.getParentFile()).getContents();
|
||||||
String name = file.getName();
|
String name = file.getName();
|
||||||
for (Entry e : dir) {
|
for (Entry e : dir) {
|
||||||
if (name.equals(e.name)) {
|
if (name.equals(e.name)) {
|
||||||
|
|
@ -149,11 +132,46 @@ public class EntryDao {
|
||||||
|
|
||||||
void generateEntries(File directory, Properties cachedProps) throws JpegProcessingException, MetadataException, FileNotFoundException, IOException {
|
void generateEntries(File directory, Properties cachedProps) throws JpegProcessingException, MetadataException, FileNotFoundException, IOException {
|
||||||
|
|
||||||
File[] files = directory.listFiles(new FileFilter() {
|
generateFileEntries(directory, cachedProps);
|
||||||
|
File dst = new File(directory, CACHE_FILE);
|
||||||
|
if (directory.canWrite()) {
|
||||||
|
cachedProps.store(new FileOutputStream(dst), "Extra Comments");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateDirectoryEntries(File directory, Album album) throws FileNotFoundException, JpegProcessingException, MetadataException, IOException, ParseException {
|
||||||
|
|
||||||
|
File[] dirs = directory.listFiles(new FileFilter() {
|
||||||
|
|
||||||
public boolean accept(File file) {
|
public boolean accept(File file) {
|
||||||
|
|
||||||
return !file.isHidden() && !file.isDirectory() && !CACHE_FILE.equals(file.getName()) && !OVERRIDE_FILE.equals(file.getName());
|
return !file.isHidden() && file.isDirectory() && !CACHE_FILE.equals(file.getName()) && !OVERRIDE_FILE.equals(file.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
for (File dir : dirs) {
|
||||||
|
|
||||||
|
Album childAlbum = read(dir);
|
||||||
|
Entry childCover = childAlbum.getCover();
|
||||||
|
if (childCover != null) {
|
||||||
|
childCover.setFile(false);
|
||||||
|
childCover.setName(dir.getName());
|
||||||
|
childCover.setPath(dir.getName() + "/" + childCover.getPath());
|
||||||
|
album.addContents(childCover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void generateFileEntries(File directory, Properties cachedProps)
|
||||||
|
throws JpegProcessingException, MetadataException, IOException {
|
||||||
|
File[] files = directory.listFiles(new FileFilter() {
|
||||||
|
|
||||||
|
public boolean accept(File file) {
|
||||||
|
|
||||||
|
String name = file.getName();
|
||||||
|
boolean isImageFile = name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".JPG");
|
||||||
|
return isImageFile && !file.isHidden() && !file.isDirectory() && !CACHE_FILE.equals(file.getName()) && !OVERRIDE_FILE.equals(file.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
@ -213,11 +231,7 @@ public class EntryDao {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File dst = new File(directory, CACHE_FILE);
|
}
|
||||||
if (directory.canWrite()) {
|
|
||||||
cachedProps.store(new FileOutputStream(dst), "Extra Comments");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Dimension decodeImageForDimensions(File file) throws IOException {
|
Dimension decodeImageForDimensions(File file) throws IOException {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,13 @@ public class AlbumServlet
|
||||||
pathInfo = pathInfo.substring(0, pathInfo.length() - ".photo".length());
|
pathInfo = pathInfo.substring(0, pathInfo.length() - ".photo".length());
|
||||||
page = "photo";
|
page = "photo";
|
||||||
}
|
}
|
||||||
|
int parentPos = pathInfo.substring(0, pathInfo.length() - 1).lastIndexOf('/');
|
||||||
|
if (parentPos >= 0) {
|
||||||
|
req.setAttribute("parent", req.getServletPath() + pathInfo.substring(0, parentPos) + "/");
|
||||||
|
}
|
||||||
|
req.setAttribute("assets", "/" + req.getContextPath() + "assets");
|
||||||
File file = new File(basePath + pathInfo);
|
File file = new File(basePath + pathInfo);
|
||||||
// System.out.println("path=" + file);
|
// System.out.println("path=" + req.getContextPath());
|
||||||
if (!file.canRead()) {
|
if (!file.canRead()) {
|
||||||
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||||
return;
|
return;
|
||||||
|
|
@ -61,7 +66,7 @@ public class AlbumServlet
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
try {
|
try {
|
||||||
List<Entry> entries = dao.read(file);
|
List<Entry> entries = dao.read(file).getContents();
|
||||||
res.setContentType("text/html");
|
res.setContentType("text/html");
|
||||||
req.setAttribute("directory", file.getName());
|
req.setAttribute("directory", file.getName());
|
||||||
req.setAttribute("path", req.getServletPath());
|
req.setAttribute("path", req.getServletPath());
|
||||||
|
|
@ -169,7 +174,7 @@ public class AlbumServlet
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getServletInfo() {
|
public String getServletInfo() {
|
||||||
return "Display a directory as an org.forkalsrud.album";
|
return "Display of org.forkalsrud.album";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
a:link, a:visited {
|
a:link, a:visited {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #4c4c4c;
|
color: #4c4c4c;
|
||||||
|
|
@ -30,18 +33,27 @@
|
||||||
img {
|
img {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
.nav {
|
||||||
|
border: 0 none;
|
||||||
|
padding: 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>$directory</h1>
|
<h1>#if($prev && $prev.isFile())<a href="${prev.name}.photo"><img class="nav" src="${assets}/left.png"/></a>#else<img class="nav" src="${assets}/left-inactive.png"/>#end#if($parent)<a href="$parent"><img class="nav" src="${assets}/up.png"/></a>#else<img class="nav" src="${assets}/up-inactive.png"/>#end#if($next && $next.isFile())<a href="${next.name}.photo"><img class="nav" src="${assets}/right.png"/></a>#else<img class="nav" src="${assets}/right-inactive.png"/>#end$directory</h1>
|
||||||
<hr/>
|
<hr/>
|
||||||
#set($thmb = 150)
|
#set($thmb = 150)
|
||||||
#foreach($entry in $entries)
|
#foreach($entry in $entries)
|
||||||
#set($dim = $entry.size.scaled($thmb))
|
#set($dim = $entry.size.scaled($thmb))
|
||||||
<div class="home_entry_table">
|
<div class="home_entry_table">
|
||||||
<span class="name">$entry.name</span><br/>
|
<span class="name">$entry.name</span><br/>
|
||||||
<a href="${entry.name}.photo"><img src="$entry.name?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
|
#if($entry.isFile())
|
||||||
|
<a href="${entry.name}.photo"><img src="$entry.path?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
|
||||||
|
#else
|
||||||
|
<a href="${entry.name}/"><img src="$entry.path?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
|
||||||
|
#end
|
||||||
<span class="caption">$!entry.caption</span>
|
<span class="caption">$!entry.caption</span>
|
||||||
</div>
|
</div>
|
||||||
#end
|
#end
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
a:link, a:visited {
|
a:link, a:visited {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #4c4c4c;
|
color: #4c4c4c;
|
||||||
|
|
@ -30,23 +33,21 @@
|
||||||
img {
|
img {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
.nav {
|
||||||
|
border: 0 none;
|
||||||
|
padding: 0px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<center>
|
<h1>#if($prev && $prev.isFile())<a href="${prev.name}.photo"><img class="nav" src="${assets}/left.png"/></a>#else<img class="nav" src="${assets}/left-inactive.png"/>#end#if($parent)<a href="$parent"><img class="nav" src="${assets}/up.png"/></a>#else<img class="nav" src="${assets}/up-inactive.png"/>#end#if($next && $next.isFile())<a href="${next.name}.photo"><img class="nav" src="${assets}/right.png"/></a>#else<img class="nav" src="${assets}/right-inactive.png"/>#end$entry.name</h1>
|
||||||
#if($prev)
|
<hr/>
|
||||||
<a href="${prev.name}.photo"><--</a>
|
|
||||||
#end
|
|
||||||
#if($next)
|
|
||||||
<a href="${next.name}.photo">--></a>
|
|
||||||
#end
|
|
||||||
</center>
|
|
||||||
|
|
||||||
#set($thmb = 480)
|
#set($thmb = 480)
|
||||||
#set($dim = $entry.size.scaled($thmb))
|
#set($dim = $entry.size.scaled($thmb))
|
||||||
<div class="photo">
|
<div class="photo">
|
||||||
<span class="name">$entry.name</span><br/>
|
<span class="name"></span><br/>
|
||||||
<a href="$entry.name"><img src="$entry.name?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
|
<a href="$entry.name"><img src="$entry.name?size=$thmb" border="0" width="$dim.width" height="$dim.height"/></a><br/>
|
||||||
<span class="caption">$!entry.caption</span>
|
<span class="caption">$!entry.caption</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
BIN
webapp/assets/left-inactive.png
Normal file
BIN
webapp/assets/left-inactive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 499 B |
BIN
webapp/assets/left.png
Normal file
BIN
webapp/assets/left.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 540 B |
BIN
webapp/assets/right-inactive.png
Normal file
BIN
webapp/assets/right-inactive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 509 B |
BIN
webapp/assets/right.png
Normal file
BIN
webapp/assets/right.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 550 B |
BIN
webapp/assets/up-inactive.png
Normal file
BIN
webapp/assets/up-inactive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 443 B |
BIN
webapp/assets/up.png
Normal file
BIN
webapp/assets/up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 511 B |
Loading…
Add table
Reference in a new issue