Update from master
authorNiki Roo <niki@nikiroo.be>
Wed, 13 May 2020 12:26:22 +0000 (14:26 +0200)
committerNiki Roo <niki@nikiroo.be>
Wed, 13 May 2020 12:26:22 +0000 (14:26 +0200)
Instance.java
library/BasicLibrary.java
library/WebLibraryServer.java
library/web/clear-32x32.png [new file with mode: 0644]
library/web/clear.png [new file with mode: 0644]
library/web/style.css

index a2cb90aa637ea700554ada2b233c5aa2649c3bd7..e0a072714a08a35b87f4342a0b4046c0abe0894c 100644 (file)
@@ -363,6 +363,11 @@ public class Instance {
                remoteDir.mkdirs();
 
                if (host != null) {
+                       host = host.replace("fanfix://", "");
+                       host = host.replace("http://", "");
+                       host = host.replace("https://", "");
+                       host = host.replaceAll("[^a-zA-Z0-9=+.-]", "_");
+                       
                        return new File(remoteDir, host);
                }
 
index d435f8d7ec7fd3e2f09242339eefb9ff6fa74d93..78f0f6208d098dcaf410e85c631dc3808330a2bd 100644 (file)
@@ -4,11 +4,9 @@ import java.io.File;
 import java.io.IOException;
 import java.net.URL;
 import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
 
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.data.MetaData;
@@ -122,12 +120,24 @@ abstract public class BasicLibrary {
         */
        public abstract Image getCover(String luid) throws IOException;
 
-       // TODO: ensure it is the main used interface
+       /**
+        * Retrieve the list of {@link MetaData} known by this {@link BasicLibrary}
+        * in a easy-to-filter version.
+        * 
+        * @param pg
+        *            the optional {@link Progress}
+        * @return the list of {@link MetaData} as a {@link MetaResultList} you can
+        *         query
+        * @throws IOException
+        *             in case of I/O eror
+        */
        public MetaResultList getList(Progress pg) throws IOException {
+               // TODO: ensure it is the main used interface
+
                return new MetaResultList(getMetas(pg));
        }
 
-       // TODO: make something for (normal and custom) not-story covers
+       // TODO: make something for (normal and custom) non-story covers
 
        /**
         * Return the cover image associated to this source.
@@ -378,6 +388,9 @@ abstract public class BasicLibrary {
        }
 
        /**
+        * @return the same as getList()
+        * @throws IOException
+        *             in case of I/O error
         * @deprecated please use {@link BasicLibrary#getList()} and
         *             {@link MetaResultList#getSources()} instead.
         */
@@ -387,6 +400,9 @@ abstract public class BasicLibrary {
        }
 
        /**
+        * @return the same as getList()
+        * @throws IOException
+        *             in case of I/O error
         * @deprecated please use {@link BasicLibrary#getList()} and
         *             {@link MetaResultList#getSourcesGrouped()} instead.
         */
@@ -396,6 +412,9 @@ abstract public class BasicLibrary {
        }
 
        /**
+        * @return the same as getList()
+        * @throws IOException
+        *             in case of I/O error
         * @deprecated please use {@link BasicLibrary#getList()} and
         *             {@link MetaResultList#getAuthors()} instead.
         */
@@ -405,9 +424,13 @@ abstract public class BasicLibrary {
        }
 
        /**
+        * @return the same as getList()
+        * @throws IOException
+        *             in case of I/O error
         * @deprecated please use {@link BasicLibrary#getList()} and
         *             {@link MetaResultList#getAuthorsGrouped()} instead.
         */
+       @Deprecated
        public Map<String, List<String>> getAuthorsGrouped() throws IOException {
                return getList().getAuthorsGrouped();
        }
@@ -901,4 +924,49 @@ abstract public class BasicLibrary {
 
                pg.done();
        }
+
+       /**
+        * Describe a {@link Story} from its {@link MetaData} and return a list of
+        * title/value that represent this {@link Story}.
+        * 
+        * @param meta
+        *            the {@link MetaData} to represent
+        * 
+        * @return the information, translated and sorted
+        */
+       static public Map<String, String> getMetaDesc(MetaData meta) {
+               Map<String, String> metaDesc = new LinkedHashMap<String, String>();
+
+               // TODO: i18n
+
+               StringBuilder tags = new StringBuilder();
+               for (String tag : meta.getTags()) {
+                       if (tags.length() > 0) {
+                               tags.append(", ");
+                       }
+                       tags.append(tag);
+               }
+
+               // TODO: i18n
+               metaDesc.put("Author", meta.getAuthor());
+               metaDesc.put("Published on", meta.getPublisher());
+               metaDesc.put("Publication date", meta.getDate());
+               metaDesc.put("Creation date", meta.getCreationDate());
+               String count = "";
+               if (meta.getWords() > 0) {
+                       count = StringUtils.formatNumber(meta.getWords());
+               }
+               if (meta.isImageDocument()) {
+                       metaDesc.put("Number of images", count);
+               } else {
+                       metaDesc.put("Number of words", count);
+               }
+               metaDesc.put("Source", meta.getSource());
+               metaDesc.put("Subject", meta.getSubject());
+               metaDesc.put("Language", meta.getLang());
+               metaDesc.put("Tags", tags.toString());
+               metaDesc.put("URL", meta.getUrl());
+
+               return metaDesc;
+       }
 }
index 3e2c90605de78b2956290b4b36b56bb8a6a42688..b4a6e4bb25f3776c819a43e9235f58b5c63b7788 100644 (file)
@@ -242,8 +242,10 @@ public class WebLibraryServer implements Runnable {
                                        // set options
                                        String optionName = params.get("optionName");
                                        if (optionName != null && !optionName.isEmpty()) {
+                                               String optionNo = params.get("optionNo");
                                                String optionValue = params.get("optionValue");
-                                               if (optionValue == null || optionValue.isEmpty()) {
+                                               if (optionNo != null || optionValue == null
+                                                               || optionValue.isEmpty()) {
                                                        session.getCookies().delete(optionName);
                                                        cookies.remove(optionName);
                                                } else {
@@ -491,12 +493,15 @@ public class WebLibraryServer implements Runnable {
 
                appendPreHtml(builder, true);
 
+               Map<String, String> params = session.getParms();
+
                String filter = cookies.get("filter");
+               if (params.get("optionNo") != null)
+                       filter = null;
                if (filter == null) {
                        filter = "";
                }
 
-               Map<String, String> params = session.getParms();
                String browser = params.get("browser") == null ? ""
                                : params.get("browser");
                String browser2 = params.get("browser2") == null ? ""
@@ -585,11 +590,12 @@ public class WebLibraryServer implements Runnable {
 
                // TODO: javascript in realtime, using visible=false + hide [submit]
                builder.append("<div class='filter'>\n");
-               builder.append("\tFilter: \n");
+               builder.append("\t<span class='label'>Filter: </span>\n");
                builder.append(
                                "\t<input name='optionName'  type='hidden' value='filter' />\n");
                builder.append("\t<input name='optionValue' type='text'   value='"
                                + filter + "' place-holder='...' />\n");
+               builder.append("\t<input name='optionNo'  type='submit' value='x' />");
                builder.append(
                                "\t<input name='submit' type='submit' value='Filter' />\n");
                builder.append("</div>\n");
@@ -672,7 +678,8 @@ public class WebLibraryServer implements Runnable {
                // 1-based (0 = desc)
                int chapter = 0;
                if (chapterStr != null && !"cover".equals(chapterStr)
-                               && !"metadata".equals(chapterStr) && !"json".equals(chapterStr)) {
+                               && !"metadata".equals(chapterStr)
+                               && !"json".equals(chapterStr)) {
                        try {
                                chapter = Integer.parseInt(chapterStr);
                                if (chapter < 0) {
@@ -706,12 +713,14 @@ public class WebLibraryServer implements Runnable {
                                if (img != null) {
                                        in = img.newInputStream();
                                }
+                               // TODO: get correct image type
+                               mimeType = "image/png";
                        } else if ("metadata".equals(chapterStr)) {
                                MetaData meta = meta(luid, whitelist);
                                JSONObject json = JsonIO.toJson(meta);
                                mimeType = "application/json";
                                in = new ByteArrayInputStream(json.toString().getBytes());
-                       }  else if ("json".equals(chapterStr)) {
+                       } else if ("json".equals(chapterStr)) {
                                Story story = story(luid, whitelist);
                                JSONObject json = JsonIO.toJson(story);
                                mimeType = "application/json";
@@ -812,123 +821,213 @@ public class WebLibraryServer implements Runnable {
                        StringBuilder builder = new StringBuilder();
                        appendPreHtml(builder, false);
 
-                       if (chapter < 0) {
-                               builder.append(story);
-                       } else {
-                               if (chapter == 0) {
-                                       // TODO: description
-                                       chapter = 1;
-                               }
+                       // For images documents, always go to the images if not chap 0 desc
+                       if (story.getMeta().isImageDocument()) {
+                               if (chapter > 0 && paragraph <= 0)
+                                       paragraph = 1;
+                       }
 
-                               Chapter chap = null;
+                       Chapter chap = null;
+                       if (chapter <= 0) {
+                               chap = story.getMeta().getResume();
+                       } else {
                                try {
                                        chap = story.getChapters().get(chapter - 1);
                                } catch (IndexOutOfBoundsException e) {
                                        return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
                                                        NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
                                }
+                       }
 
-                               if (story.getMeta().isImageDocument() && paragraph <= 0) {
-                                       paragraph = 1;
+                       String first, previous, next, last;
+
+                       StringBuilder content = new StringBuilder();
+
+                       String disabledLeft = "";
+                       String disabledRight = "";
+                       String disabledZoomReal = "";
+                       String disabledZoomWidth = "";
+                       String disabledZoomHeight = "";
+
+                       if (paragraph <= 0) {
+                               first = getViewUrl(luid, 0, null);
+                               previous = getViewUrl(luid, (Math.max(chapter - 1, 0)), null);
+                               next = getViewUrl(luid,
+                                               (Math.min(chapter + 1, story.getChapters().size())),
+                                               null);
+                               last = getViewUrl(luid, story.getChapters().size(), null);
+
+                               StringBuilder desc = new StringBuilder();
+
+                               if (chapter <= 0) {
+                                       desc.append("<h1 class='title'>");
+                                       desc.append(story.getMeta().getTitle());
+                                       desc.append("</h1>\n");
+                                       desc.append("<div class='desc'>\n");
+                                       desc.append("\t<div class='cover'>\n");
+                                       desc.append("\t\t<img src='/story/" + luid + "/cover'/>\n");
+                                       desc.append("\t</div>\n");
+                                       desc.append("\t<table class='details'>\n");
+                                       Map<String, String> details = BasicLibrary
+                                                       .getMetaDesc(story.getMeta());
+                                       for (String key : details.keySet()) {
+                                               appendTableRow(desc, 2, key, details.get(key));
+                                       }
+                                       desc.append("\t</table>\n");
+                                       desc.append("</div>\n");
+                                       desc.append("<h1 class='title'>Description</h1>\n");
                                }
 
-                               String first, previous, next, last;
-                               String content;
+                               content.append("<div class='viewer text'>\n");
+                               content.append(desc);
+                               String description = new TextOutput(false).convert(chap,
+                                               chapter > 0);
+                               content.append(chap.getParagraphs().size() <= 0
+                                               ? "No content provided."
+                                               : description);
+                               content.append("</div>\n");
+
+                               if (chapter <= 0)
+                                       disabledLeft = " disabled='disbaled'";
+                               if (chapter >= story.getChapters().size())
+                                       disabledRight = " disabled='disbaled'";
+                       } else {
+                               first = getViewUrl(luid, chapter, 1);
+                               previous = getViewUrl(luid, chapter,
+                                               (Math.max(paragraph - 1, 1)));
+                               next = getViewUrl(luid, chapter,
+                                               (Math.min(paragraph + 1, chap.getParagraphs().size())));
+                               last = getViewUrl(luid, chapter, chap.getParagraphs().size());
+
+                               if (paragraph <= 1)
+                                       disabledLeft = " disabled='disbaled'";
+                               if (paragraph >= chap.getParagraphs().size())
+                                       disabledRight = " disabled='disbaled'";
+
+                               // First -> previous *chapter*
+                               if (chapter > 0)
+                                       disabledLeft = "";
+                               first = getViewUrl(luid, (Math.max(chapter - 1, 0)), null);
+                               if (paragraph <= 1) {
+                                       previous = first;
+                               }
 
-                               if (paragraph <= 0) {
-                                       first = getViewUrl(luid, 1, null);
-                                       previous = getViewUrl(luid, (Math.max(chapter - 1, 1)),
-                                                       null);
-                                       next = getViewUrl(luid,
-                                                       (Math.min(chapter + 1, story.getChapters().size())),
-                                                       null);
-                                       last = getViewUrl(luid, story.getChapters().size(), null);
-
-                                       content = "<div class='viewer text'>\n"
-                                                       + new TextOutput(false).convert(chap, true)
-                                                       + "</div>\n";
-                               } else {
-                                       first = getViewUrl(luid, chapter, 1);
-                                       previous = getViewUrl(luid, chapter,
-                                                       (Math.max(paragraph - 1, 1)));
-                                       next = getViewUrl(luid, chapter, (Math.min(paragraph + 1,
-                                                       chap.getParagraphs().size())));
-                                       last = getViewUrl(luid, chapter,
-                                                       chap.getParagraphs().size());
-
-                                       Paragraph para = null;
-                                       try {
-                                               para = chap.getParagraphs().get(paragraph - 1);
-                                       } catch (IndexOutOfBoundsException e) {
-                                               return NanoHTTPD.newFixedLengthResponse(
-                                                               Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
-                                                               "Paragraph not found");
-                                       }
+                               Paragraph para = null;
+                               try {
+                                       para = chap.getParagraphs().get(paragraph - 1);
+                               } catch (IndexOutOfBoundsException e) {
+                                       return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
+                                                       NanoHTTPD.MIME_PLAINTEXT,
+                                                       "Paragraph " + paragraph + " not found");
+                               }
 
-                                       if (para.getType() == ParagraphType.IMAGE) {
-                                               String zoomStyle = "max-width: 100%;";
-                                               String zoomOption = cookies.get("zoom");
-                                               if (zoomOption != null && !zoomOption.isEmpty()) {
-                                                       if (zoomOption.equals("real")) {
-                                                               zoomStyle = "";
-                                                       } else if (zoomOption.equals("width")) {
-                                                               zoomStyle = "max-width: 100%;";
-                                                       } else if (zoomOption.equals("height")) {
-                                                               // see height of navbar + optionbar
-                                                               zoomStyle = "max-height: calc(100% - 128px);";
-                                                       }
+                               if (para.getType() == ParagraphType.IMAGE) {
+                                       String zoomStyle = "max-width: 100%;";
+                                       disabledZoomWidth = " disabled='disabled'";
+                                       String zoomOption = cookies.get("zoom");
+                                       if (zoomOption != null && !zoomOption.isEmpty()) {
+                                               if (zoomOption.equals("real")) {
+                                                       zoomStyle = "";
+                                                       disabledZoomWidth = "";
+                                                       disabledZoomReal = " disabled='disabled'";
+                                               } else if (zoomOption.equals("width")) {
+                                                       zoomStyle = "max-width: 100%;";
+                                               } else if (zoomOption.equals("height")) {
+                                                       // see height of navbar + optionbar
+                                                       zoomStyle = "max-height: calc(100% - 128px);";
+                                                       disabledZoomWidth = "";
+                                                       disabledZoomHeight = " disabled='disabled'";
                                                }
-                                               content = String.format("" //
-                                                               + "<a class='viewer link' href='%s'>" //
-                                                               + "<img class='viewer img' style='%s' src='%s'/>"
-                                                               + "</a>", //
-                                                               next, //
-                                                               zoomStyle, //
-                                                               getStoryUrl(luid, chapter, paragraph));
-                                       } else {
-                                               content = para.getContent();
                                        }
 
+                                       content.append(String.format("" //
+                                                       + "<a class='viewer link' href='%s'>"
+                                                       + "<img class='viewer img' style='%s' src='%s'/>"
+                                                       + "</a>", //
+                                                       next, //
+                                                       zoomStyle, //
+                                                       getStoryUrl(luid, chapter, paragraph)));
+                               } else {
+                                       content.append(String.format("" //
+                                                       + "<div class='viewer text'>%s</div>", //
+                                                       para.getContent()));
                                }
+                       }
 
-                               builder.append(String.format("" //
-                                               + "<div class='bar navbar'>\n" //
-                                               + "\t<a class='button first' href='%s'>&lt;&lt;</a>\n"//
-                                               + "\t<a class='button previous' href='%s'>&lt;</a>\n"//
-                                               + "\t<a class='button next' href='%s'>&gt;</a>\n"//
-                                               + "\t<a class='button last' href='%s'>&gt;&gt;</a>\n"//
-                                               + "</div>\n" //
-                                               + "%s", //
-                                               first, //
-                                               previous, //
-                                               next, //
-                                               last, //
-                                               content //
-                               ));
-
-                               builder.append("<div class='bar optionbar ");
-                               if (paragraph > 0) {
-                                       builder.append("s4");
-                               } else {
-                                       builder.append("s1");
+                       builder.append(String.format("" //
+                                       + "<div class='bar navbar'>\n" //
+                                       + "\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
+                                       + "\t<a%s class='button previous' href='%s'>&lt;</a>\n" //
+                                       + "\t<div class='gotobox itemsbox'>\n" //
+                                       + "\t\t<div class='button goto'>%d</div>\n" //
+                                       + "\t\t<div class='items goto'>\n", //
+                                       disabledLeft, first, //
+                                       disabledLeft, previous, //
+                                       paragraph > 0 ? paragraph : chapter //
+                       ));
+
+                       // List of chap/para links
+
+                       String blink = "/view/story/" + luid + "/";
+                       appendItemA(builder, 3, blink + "0", "Description",
+                                       paragraph == 0 && chapter == 0);
+
+                       if (paragraph > 0) {
+                               blink = blink + chapter + "/";
+                               for (int i = 1; i <= chap.getParagraphs().size(); i++) {
+                                       appendItemA(builder, 3, blink + i, "Image " + i,
+                                                       paragraph == i);
                                }
-                               builder.append("'>\n");
-                               builder.append(
-                                               "       <a class='button back' href='/'>BACK</a>\n");
-
-                               if (paragraph > 0) {
-                                       builder.append(String.format("" //
-                                                       + "\t<a class='button zoomreal'   href='%s'>REAL</a>\n"//
-                                                       + "\t<a class='button zoomwidth'  href='%s'>WIDTH</a>\n"//
-                                                       + "\t<a class='button zoomheight' href='%s'>HEIGHT</a>\n"//
-                                                       + "</div>\n", //
-                                                       uri + "?optionName=zoom&optionValue=real", //
-                                                       uri + "?optionName=zoom&optionValue=width", //
-                                                       uri + "?optionName=zoom&optionValue=height" //
-                                       ));
+                       } else {
+                               int i = 1;
+                               for (Chapter c : story.getChapters()) {
+                                       String chapName = "Chapter " + c.getNumber();
+                                       if (c.getName() != null && !c.getName().isEmpty()) {
+                                               chapName += ": " + c.getName();
+                                       }
+
+                                       appendItemA(builder, 3, blink + i, chapName, chapter == i);
+
+                                       i++;
                                }
                        }
 
+                       builder.append(String.format("" //
+                                       + "\t\t</div>\n" //
+                                       + "\t</div>\n" //
+                                       + "\t<a%s class='button next' href='%s'>&gt;</a>\n" //
+                                       + "\t<a%s class='button last' href='%s'>&gt;&gt;</a>\n"//
+                                       + "</div>\n", //
+                                       disabledRight, next, //
+                                       disabledRight, last //
+                       ));
+
+                       builder.append(content);
+
+                       builder.append("<div class='bar optionbar ");
+                       if (paragraph > 0) {
+                               builder.append("s4");
+                       } else {
+                               builder.append("s1");
+                       }
+                       builder.append("'>\n");
+                       builder.append("        <a class='button back' href='/'>BACK</a>\n");
+
+                       if (paragraph > 0) {
+                               builder.append(String.format("" //
+                                               + "\t<a%s class='button zoomreal'   href='%s'>REAL</a>\n"//
+                                               + "\t<a%s class='button zoomwidth'  href='%s'>WIDTH</a>\n"//
+                                               + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
+                                               + "</div>\n", //
+                                               disabledZoomReal,
+                                               uri + "?optionName=zoom&optionValue=real", //
+                                               disabledZoomWidth,
+                                               uri + "?optionName=zoom&optionValue=width", //
+                                               disabledZoomHeight,
+                                               uri + "?optionName=zoom&optionValue=height" //
+                               ));
+                       }
+
                        appendPostHtml(builder);
                        return NanoHTTPD.newFixedLengthResponse(Status.OK,
                                        NanoHTTPD.MIME_HTML, builder.toString());
@@ -998,7 +1097,8 @@ public class WebLibraryServer implements Runnable {
                        throws IOException {
                MetaData meta = meta(luid, whitelist);
                if (meta != null) {
-                       return meta.getCover();
+                       BasicLibrary lib = Instance.getInstance().getLibrary();
+                       return lib.getCover(meta.getLuid());
                }
 
                return null;
@@ -1073,7 +1173,7 @@ public class WebLibraryServer implements Runnable {
 
                if (banner) {
                        builder.append("<div class='banner'>\n");
-                       builder.append("\t<img class='ico' src='") //
+                       builder.append("\t<img class='ico' src='/") //
                                        .append(favicon) //
                                        .append("'/>\n");
                        builder.append("\t<h1>Fanfix</h1>\n");
@@ -1099,4 +1199,45 @@ public class WebLibraryServer implements Runnable {
                }
                builder.append(">").append(name).append("</option>\n");
        }
+
+       private void appendTableRow(StringBuilder builder, int depth,
+                       String... tds) {
+               for (int i = 0; i < depth; i++) {
+                       builder.append("\t");
+               }
+
+               int col = 1;
+               builder.append("<tr>");
+               for (String td : tds) {
+                       builder.append("<td class='col");
+                       builder.append(col++);
+                       builder.append("'>");
+                       builder.append(td);
+                       builder.append("</td>");
+               }
+               builder.append("</tr>\n");
+       }
+
+       private void appendItemA(StringBuilder builder, int depth, String link,
+                       String name, boolean selected) {
+               for (int i = 0; i < depth; i++) {
+                       builder.append("\t");
+               }
+
+               builder.append("<a href='");
+               builder.append(link);
+               builder.append("' class='item goto");
+               if (selected) {
+                       builder.append(" selected");
+               }
+               builder.append("'>");
+               builder.append(name);
+               builder.append("</a>\n");
+       }
+
+       public static void main(String[] args) throws IOException {
+               Instance.init();
+               WebLibraryServer web = new WebLibraryServer(false);
+               web.run();
+       }
 }
diff --git a/library/web/clear-32x32.png b/library/web/clear-32x32.png
new file mode 100644 (file)
index 0000000..bdc51d0
Binary files /dev/null and b/library/web/clear-32x32.png differ
diff --git a/library/web/clear.png b/library/web/clear.png
new file mode 100644 (file)
index 0000000..db0509f
Binary files /dev/null and b/library/web/clear.png differ
index c520d78f2230fc2a8dad5e95f13f768f87f540d6..4196aeba5a52a69d1a23a7ccc38500a28470729e 100644 (file)
@@ -1,16 +1,26 @@
+/*
+ * #AAA391: main gray
+ * #555C7E: reverse main
+ *
+ * #bac2e1: button border
+ * #DCE4F3: button hover
+ *
+ * #ddffdd: background pastel green
+ * #88dd88: for border
+ *
+ * #ffdddd: pastel red
+ * #dd8888: for border
+ *
+ */
+
 html, body, .main {
        margin: 0;
        padding: 0;
-       font-family : Verdana, "Bitstream Vera Sans", "DejaVu Sans", Tahoma, Geneva, Arial, Sans-serif;
+       font-family: Verdana, "Bitstream Vera Sans", "DejaVu Sans", Tahoma, Geneva, Arial, Sans-serif;
        font-size: 12px;
-       DISABLED_color: #635c4a;
        height: 100%;
 }
 
-table {
-       width: 100%;
-}
-
 .banner {
 }
 
@@ -25,25 +35,25 @@ table {
 }
 
 .main {
-       display: block;
+       margin: auto;
 }
 
 .message {
-        background-color: #ddffdd;
-        border: 1px solid #88dd88;
-        clear: left;
-        border-radius: 5px;
-        padding: 5px;
-        margin: 10px;
+       background-color: #ddffdd;
+       border: 1px solid #88dd88;
+       clear: left;
+       border-radius: 5px;
+       padding: 5px;
+       margin: 10px;
 }
 
 .error {
-        background-color: #ffdddd;
-        border: 1px solid #dd8888;
-        clear: left;
-        border-radius: 5px;
-        padding: 5px;
-        margin: 10px;
+       background-color: #ffdddd;
+       border: 1px solid #dd8888;
+       clear: left;
+       border-radius: 5px;
+       padding: 5px;
+       margin: 10px;
 }
 
 /* all links and clickable should show a pointer cursor */
@@ -51,7 +61,7 @@ table {
        cursor: pointer;
 }
 
-a:hover {
+a:not(.selected):hover {
        background-color: rgb(225, 225, 225);
 }
 
@@ -64,13 +74,62 @@ h3 {
        margin-left: 20px;
 }
 
+.itemsbox .items {
+       display: block;
+       position: absolute;
+       overflow-y: auto;
+       
+       transition: all 0.5s ease;
+       visibility: hidden;
+       opacity: 0;
+}
+
+.itemsbox:hover .items, .itemsbox .items:hover {
+       visibility: visible;
+       opacity: 1;
+}
+
+.itemsbox .items .item {
+       display: block;
+       padding: 5px;
+       color: black;
+       cursor: pointer;
+       text-decoration: none;
+}
+
+.itemsbox .items .item.selected {
+       font-weight: bold;
+       cursor: default;
+}
+
+.itemsbox .items .item:not(.selected):hover, .button:hover {
+       background-color: #DCE4F3;
+}
+
+.itemsbox .items .item[disabled], .button[disabled] {
+       pointer-events: none;
+       background-color: #aaa;
+       filter: grayscale(100%);
+       border-color: #AAA391;
+}
+
+.button {
+       display: inline-block;
+       text-align: center;
+       text-decoration: none;
+       background-position: center;
+       background-repeat: no-repeat;
+       border: 1px solid #bac2e1;
+       border-radius: 5px;
+}
+
 .login {
        width: 250px;
        display: flex;
        margin: auto;
-       margin-top: 200px;
+       margin-top: 100px;
        flex-direction: column;
-       border: 1px solid gray;
+       border: 1px solid #AAA391;
        padding: 20px;
        border-radius: 10px;
 }
@@ -88,7 +147,39 @@ h3 {
 }
 
 .filter {
-       padding: 10px;
+       padding: 5px;
+       height: 36px;
+       border: 1px solid #AAA391;
+       border-radius: 5px;
+       margin-top: 5px;
+       margin-bottom: 5px;
+       display: flex;
+}
+
+.filter > * {
+       height: 36px;
+       display: inline-block;
+}
+
+.filter .label {
+       line-height: 36px;
+       padding-right: 5px;
+}
+
+.filter input[type='text'] {
+       height: 32px;
+       padding: 0;
+       margin: 0;
+       border-width: 2px;
+       min-width: 50px;
+       flex-grow: 100;
+       flex-shrink: 100;
+}
+
+.filter input[name='optionNo'] {
+       background-image: url('/clear-32x32.png');
+       color: transparent;
+       width: 36px;
 }
 
 .books {
@@ -101,8 +192,7 @@ h3 {
 
 .book_line .link, .book_line .title {
        flex-grow: 100;
-       padding-right: 5px;
-       padding-left: 5px;
+       padding: 5px;
 }
 
 .book_line .link {
@@ -114,7 +204,7 @@ h3 {
 }
 
 .book_line .luid {
-       color: gray;
+       color: #AAA391;
        padding-right: 10px;
        padding-left: 10px;
 }
@@ -134,13 +224,66 @@ h3 {
        display: block;
        background: white;
        position: fixed;
+       margin: auto;
 }
 
+.bar.navbar       { padding-left: calc(50% - (5 * (64px + 5px) / 2)); }
+
+.bar.optionbar.s1 { padding-left: calc(50% - (1 * (64px + 5px) / 2)); }
+.bar.optionbar.s4 { padding-left: calc(50% - (4 * (64px + 5px) / 2)); }
+
+.bar.optionbar {
+       bottom: 0;
+}
+
+.bar .button {
+       height: 54px;
+       width: 54px;
+       line-height: 54px;
+       margin: 5px;
+}
+
+.bar .gotobox {
+       display: inline-block;
+}
+
+.bar .gotobox .items {
+       background-color: white;
+       border: 1px solid gray;
+       border-radius: 5px;
+       width: 196px;
+       margin: -5px;
+       margin-left: -66px;
+       max-height: 400px;
+}
+
+.bar .button.first    { color: transparent; background-image: url('/arrow_double_left-32x32.png');  }
+.bar .button.previous { color: transparent; background-image: url('/arrow_left-32x32.png');         }
+.bar .button.next     { color: transparent; background-image: url('/arrow_right-32x32.png');        }
+.bar .button.last     { color: transparent; background-image: url('/arrow_double_right-32x32.png'); }
+
+.bar .button.back       { color: transparent; background-image: url('/back-32x32.png');          }
+.bar .button.zoomreal   { color: transparent; background-image: url('/actual_size-32x32.png');   }
+.bar .button.zoomwidth  { color: transparent; background-image: url('/fit_to_width-32x32.png');  }
+.bar .button.zoomheight { color: transparent; background-image: url('/fit_to_height-32x32.png'); }
+
 .viewer {
        padding-top: 64px;
        padding-bottom: 64px;
 }
 
+.viewer .title {
+       background-color: #AAA391;
+       text-align: center;
+       font-variant: small-caps;
+       color: white;
+}
+
+.viewer.img {
+       margin: auto;
+       display: block;
+}
+
 a.viewer.link:hover {
        background-color: transparent;
 }
@@ -150,47 +293,46 @@ a.viewer.link:hover {
        padding-right: 10px;
 }
 
-.bar.navbar {
-       padding-left: calc(50% - (4 * 64px / 2));
+.desc {
+       display: flex;
+       flex-direction: column;
+       padding-top: 10px;
 }
 
-.bar.optionbar {
-       bottom: 0;      
+.desc .cover {
+       max-height: 280px;
+       overflow: hidden;
 }
 
-.bar.optionbar.s1 {
-       padding-left: calc(50% - (1 * 64px / 2));
+.desc .cover img {
+       max-width: 80%;
+       margin: auto;
+       display: block;
 }
 
-.bar.optionbar.s4 {
-       padding-left: calc(50% - (4 * 64px / 2));
+.desc .details .col1 {
+       font-weight: bold;
+       padding-left: 5px;
+       padding-right: 5px; 
 }
 
-.bar .button {
-       height: 54px;
-       width: 54px;
-       line-height: 64px;
-       display: inline-block;
-       text-align: center;
-       color: transparent;
-       text-decoration: none;
-       background-position: center;
-       background-repeat: no-repeat;
-       border-radius: 5px;
-       border: 1px solid #bac2e1;
-       margin: 5px;
+.desc .details .col2 {
+       color: #AAA391;
 }
 
-.bar .button:hover {
-       background-color: bac2e1;
+@media(min-width: 800px) {
+        .main {
+               max-width: 800px;
+        }
+        
+       .bar.navbar       { padding-left: calc(400px - (5 * (64px + 5px) / 2)); }
+       
+       .bar.optionbar.s1 { padding-left: calc(400px - (1 * (64px + 5px) / 2)); }
+       .bar.optionbar.s4 { padding-left: calc(400px - (4 * (64px + 5px) / 2)); }
+       
+       .desc { flex-direction: row; }
+       .desc .cover img {
+               width: 200px;
+               padding-right: 10px;
+       }
 }
-
-.bar .button.first    { background-image: url('/arrow_double_left-32x32.png');  }
-.bar .button.previous { background-image: url('/arrow_left-32x32.png');         }
-.bar .button.next     { background-image: url('/arrow_right-32x32.png');        }
-.bar .button.last     { background-image: url('/arrow_double_right-32x32.png'); }
-
-.bar .button.back       { background-image: url('/back-32x32.png');          }
-.bar .button.zoomreal   { background-image: url('/actual_size-32x32.png');   }
-.bar .button.zoomwidth  { background-image: url('/fit_to_width-32x32.png');  }
-.bar .button.zoomheight { background-image: url('/fit_to_height-32x32.png'); }