WebServer: add story description
authorNiki Roo <niki@nikiroo.be>
Wed, 13 May 2020 08:46:07 +0000 (10:46 +0200)
committerNiki Roo <niki@nikiroo.be>
Wed, 13 May 2020 08:46:07 +0000 (10:46 +0200)
src/be/nikiroo/fanfix/library/BasicLibrary.java
src/be/nikiroo/fanfix/library/WebLibraryServer.java
src/be/nikiroo/fanfix/library/web/style.css

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 7c5cf425bd0dd21394c9e4e881c33c5657530ae2..fe79f96ad455c0814e0c759196ff580c37609733 100644 (file)
@@ -93,8 +93,8 @@ public class WebLibraryServer implements Runnable {
                                                subkeys.add("");
 
                                                for (String subkey : subkeys) {
-                                                       if (CookieUtils.validateCookie(wookie + subkey
-                                                                       + opts, rehashed)) {
+                                                       if (CookieUtils.validateCookie(
+                                                                       wookie + subkey + opts, rehashed)) {
                                                                this.wookie = wookie;
                                                                this.token = token;
                                                                this.success = true;
@@ -148,7 +148,8 @@ public class WebLibraryServer implements Runnable {
                Integer port = Instance.getInstance().getConfig()
                                .getInteger(Config.SERVER_PORT);
                if (port == null) {
-                       throw new IOException("Cannot start web server: port not specified");
+                       throw new IOException(
+                                       "Cannot start web server: port not specified");
                }
 
                int cacheMb = Instance.getInstance().getConfig()
@@ -172,8 +173,8 @@ public class WebLibraryServer implements Runnable {
                        if (!keystorePath.isEmpty()) {
                                File keystoreFile = new File(keystorePath);
                                try {
-                                       KeyStore keystore = KeyStore.getInstance(KeyStore
-                                                       .getDefaultType());
+                                       KeyStore keystore = KeyStore
+                                                       .getInstance(KeyStore.getDefaultType());
                                        InputStream keystoreStream = new FileInputStream(
                                                        keystoreFile);
                                        try {
@@ -225,8 +226,8 @@ public class WebLibraryServer implements Runnable {
                                                        params.get("login"), whitelist);
                                } else {
                                        String token = cookies.get("token");
-                                       login = login(who, token, Instance.getInstance()
-                                                       .getConfig().getList(Config.SERVER_ALLOWED_SUBKEYS));
+                                       login = login(who, token, Instance.getInstance().getConfig()
+                                                       .getList(Config.SERVER_ALLOWED_SUBKEYS));
                                }
 
                                if (login.isSuccess()) {
@@ -235,21 +236,21 @@ public class WebLibraryServer implements Runnable {
                                        }
 
                                        // refresh token
-                                       session.getCookies()
-                                                       .set(new Cookie("token", login.getToken(),
-                                                                       "30; path=/"));
+                                       session.getCookies().set(new Cookie("token",
+                                                       login.getToken(), "30; path=/"));
 
                                        // 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 {
-                                                       session.getCookies().set(
-                                                                       new Cookie(optionName, optionValue,
-                                                                                       "; path=/"));
+                                                       session.getCookies().set(new Cookie(optionName,
+                                                                       optionValue, "; path=/"));
                                                        cookies.put(optionName, optionValue);
                                                }
                                        }
@@ -259,7 +260,7 @@ public class WebLibraryServer implements Runnable {
                                if (!login.isSuccess() && (uri.equals("/") //
                                                || uri.startsWith(STORY_URL_BASE) //
                                                || uri.startsWith(VIEWER_URL_BASE) //
-                               || uri.startsWith(LIST_URL))) {
+                                               || uri.startsWith(LIST_URL))) {
                                        rep = loginPage(login, uri);
                                }
 
@@ -303,10 +304,9 @@ public class WebLibraryServer implements Runnable {
                                                                        NanoHTTPD.MIME_PLAINTEXT, "Not Found");
                                                }
                                        } catch (Exception e) {
-                                               Instance.getInstance()
-                                                               .getTraceHandler()
-                                                               .error(new IOException(
-                                                                               "Cannot process web request", e));
+                                               Instance.getInstance().getTraceHandler().error(
+                                                               new IOException("Cannot process web request",
+                                                                               e));
                                                rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
                                                                NanoHTTPD.MIME_PLAINTEXT, "An error occured");
                                        }
@@ -415,8 +415,8 @@ public class WebLibraryServer implements Runnable {
                        wl = false;
                }
 
-               rw = Instance.getInstance().getConfig()
-                               .getBoolean(Config.SERVER_RW, rw);
+               rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
+                               rw);
                if (!subkey.isEmpty()) {
                        List<String> allowed = Instance.getInstance().getConfig()
                                        .getList(Config.SERVER_ALLOWED_SUBKEYS);
@@ -450,9 +450,10 @@ public class WebLibraryServer implements Runnable {
                        uri = "/";
                }
 
-               builder.append("<form method='POST' action='" + uri
-                               + "' class='login'>\n");
-               builder.append("<p>You must be logged into the system to see the stories.</p>");
+               builder.append(
+                               "<form method='POST' action='" + uri + "' class='login'>\n");
+               builder.append(
+                               "<p>You must be logged into the system to see the stories.</p>");
                builder.append("\t<input type='text' name='login' />\n");
                builder.append("\t<input type='password' name='password' />\n");
                builder.append("\t<input type='submit' value='Login' />\n");
@@ -475,8 +476,8 @@ public class WebLibraryServer implements Runnable {
                        }
 
                        return newInputStreamResponse("application/json",
-                                       new ByteArrayInputStream(new JSONArray(jsons).toString()
-                                                       .getBytes()));
+                                       new ByteArrayInputStream(
+                                                       new JSONArray(jsons).toString().getBytes()));
                }
 
                return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
@@ -490,24 +491,23 @@ public class WebLibraryServer implements Runnable {
                result = new MetaResultList(result.filter(whitelist, null, null));
                StringBuilder builder = new StringBuilder();
 
-               
                appendPreHtml(builder, true);
 
                Map<String, String> params = session.getParms();
-               
+
                String filter = cookies.get("filter");
-               if (params.get("clear") != null)
+               if (params.get("optionNo") != null)
                        filter = null;
                if (filter == null) {
                        filter = "";
                }
 
-               String browser = params.get("browser") == null ? "" : params
-                               .get("browser");
-               String browser2 = params.get("browser2") == null ? "" : params
-                               .get("browser2");
-               String browser3 = params.get("browser3") == null ? "" : params
-                               .get("browser3");
+               String browser = params.get("browser") == null ? ""
+                               : params.get("browser");
+               String browser2 = params.get("browser2") == null ? ""
+                               : params.get("browser2");
+               String browser3 = params.get("browser3") == null ? ""
+                               : params.get("browser3");
 
                String filterSource = null;
                String filterAuthor = null;
@@ -590,30 +590,33 @@ 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<input name='optionName'  type='hidden' value='filter' />\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='clear'  type='submit' value='x' />");
-               builder.append("\t<input name='submit' type='submit' value='Filter' />\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");
                builder.append("</form>\n");
 
                builder.append("\t<div class='books'>");
                for (MetaData meta : result.getMetas()) {
-                       if (!filter.isEmpty()
-                                       && !meta.getTitle().toLowerCase()
-                                                       .contains(filter.toLowerCase())) {
+                       if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
+                                       .contains(filter.toLowerCase())) {
                                continue;
                        }
 
                        // TODO Sub sources
-                       if (filterSource != null && !filterSource.equals(meta.getSource())) {
+                       if (filterSource != null
+                                       && !filterSource.equals(meta.getSource())) {
                                continue;
                        }
 
                        // TODO: sub authors
-                       if (filterAuthor != null && !filterAuthor.equals(meta.getAuthor())) {
+                       if (filterAuthor != null
+                                       && !filterAuthor.equals(meta.getAuthor())) {
                                continue;
                        }
 
@@ -629,10 +632,12 @@ public class WebLibraryServer implements Runnable {
 
                        if (lib.isCached(meta.getLuid())) {
                                // â—‰ = &#9673;
-                               builder.append("<span class='cache_icon cached'>&#9673;</span>");
+                               builder.append(
+                                               "<span class='cache_icon cached'>&#9673;</span>");
                        } else {
                                // â—‹ = &#9675;
-                               builder.append("<span class='cache_icon uncached'>&#9675;</span>");
+                               builder.append(
+                                               "<span class='cache_icon uncached'>&#9675;</span>");
                        }
                        builder.append("<span class='luid'>");
                        builder.append(meta.getLuid());
@@ -673,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) {
@@ -729,8 +735,8 @@ public class WebLibraryServer implements Runnable {
                                                        builder.append(p.getContent());
                                                }
 
-                                               in = new ByteArrayInputStream(builder.toString()
-                                                               .getBytes("utf-8"));
+                                               in = new ByteArrayInputStream(
+                                                               builder.toString().getBytes("utf-8"));
                                        } else {
                                                Paragraph para = story.getChapters().get(chapter - 1)
                                                                .getParagraphs().get(paragraph - 1);
@@ -740,8 +746,8 @@ public class WebLibraryServer implements Runnable {
                                                        mimeType = "image/png";
                                                        in = img.newInputStream();
                                                } else {
-                                                       in = new ByteArrayInputStream(para.getContent()
-                                                                       .getBytes("utf-8"));
+                                                       in = new ByteArrayInputStream(
+                                                                       para.getContent().getBytes("utf-8"));
                                                }
                                        }
                                }
@@ -813,11 +819,9 @@ public class WebLibraryServer implements Runnable {
                        StringBuilder builder = new StringBuilder();
                        appendPreHtml(builder, false);
 
-                       // TODO: no desc page for images?
+                       // For images documents, always go to the images if not chap 0 desc
                        if (story.getMeta().isImageDocument()) {
-                               if (chapter <= 0)
-                                       chapter = 1;
-                               if (paragraph <= 0)
+                               if (chapter > 0 && paragraph <= 0)
                                        paragraph = 1;
                        }
 
@@ -844,14 +848,13 @@ public class WebLibraryServer implements Runnable {
                        String disabledZoomHeight = "";
 
                        if (paragraph <= 0) {
-                               first = getViewUrl(luid, 1, null);
-                               previous = getViewUrl(luid, (Math.max(chapter - 1, 1)), null);
+                               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);
 
-                               // TODO
                                StringBuilder desc = new StringBuilder();
 
                                if (chapter <= 0) {
@@ -859,10 +862,12 @@ public class WebLibraryServer implements Runnable {
                                        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>\n");
-                                       desc.append("\t\t<tr><th>HEAD 1</th><th>HEAD 2</th></tr>\n");
-                                       desc.append("\t\t<tr><td>KEY 1</td><td>VAL 1</td></tr>\n");
-                                       desc.append("\t\t<tr><td>KEY 2</td><td>VAL 2</td></tr>\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");
@@ -870,10 +875,14 @@ public class WebLibraryServer implements Runnable {
 
                                content.append("<div class='viewer text'>\n");
                                content.append(desc);
-                               content.append(new TextOutput(false).convert(chap, chapter > 0));
+                               String description = new TextOutput(false).convert(chap,
+                                               chapter > 0);
+                               content.append(
+                                               description.isEmpty() ? "No description provided."
+                                                               : description);
                                content.append("</div>\n");
 
-                               if (chapter <= 1)
+                               if (chapter <= 0)
                                        disabledLeft = " disabled='disbaled'";
                                if (chapter >= story.getChapters().size())
                                        disabledRight = " disabled='disbaled'";
@@ -890,13 +899,21 @@ public class WebLibraryServer implements Runnable {
                                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;
+                               }
+
                                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");
+                                                       NanoHTTPD.MIME_PLAINTEXT,
+                                                       "Paragraph " + paragraph + " not found");
                                }
 
                                if (para.getType() == ParagraphType.IMAGE) {
@@ -926,7 +943,9 @@ public class WebLibraryServer implements Runnable {
                                                        zoomStyle, //
                                                        getStoryUrl(luid, chapter, paragraph)));
                                } else {
-                                       content.append(para.getContent());
+                                       content.append(String.format("" //
+                                                       + "<div class='viewer text'>%s</div>", //
+                                                       para.getContent()));
                                }
                        }
 
@@ -941,7 +960,7 @@ public class WebLibraryServer implements Runnable {
                                        disabledLeft, previous, //
                                        disabledRight, next, //
                                        disabledRight, last //
-                                       ));
+                       ));
 
                        builder.append(content);
 
@@ -955,19 +974,18 @@ public class WebLibraryServer implements Runnable {
                        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" //
-                                               ));
+                               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);
@@ -983,23 +1001,21 @@ public class WebLibraryServer implements Runnable {
 
        private Response newInputStreamResponse(String mimeType, InputStream in) {
                if (in == null) {
-                       return NanoHTTPD
-                                       .newFixedLengthResponse(Status.NO_CONTENT, "", null);
+                       return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "",
+                                       null);
                }
                return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
        }
 
        private String getContentOf(String file) {
-               InputStream in = IOUtils
-                               .openResource(WebLibraryServerIndex.class, file);
+               InputStream in = IOUtils.openResource(WebLibraryServerIndex.class,
+                               file);
                if (in != null) {
                        try {
                                return IOUtils.readSmallStream(in);
                        } catch (IOException e) {
-                               Instance.getInstance()
-                                               .getTraceHandler()
-                                               .error(new IOException(
-                                                               "Cannot get file: index.pre.html", e));
+                               Instance.getInstance().getTraceHandler().error(
+                                               new IOException("Cannot get file: index.pre.html", e));
                        }
                }
 
@@ -1049,7 +1065,8 @@ public class WebLibraryServer implements Runnable {
        }
 
        // NULL if not whitelist OK or if not found
-       private Story story(String luid, List<String> whitelist) throws IOException {
+       private Story story(String luid, List<String> whitelist)
+                       throws IOException {
                synchronized (storyCache) {
                        if (storyCache.containsKey(luid)) {
                                Story story = storyCache.get(luid);
@@ -1111,8 +1128,8 @@ public class WebLibraryServer implements Runnable {
                        favicon = "icon_" + icon.replace("-", "_") + ".png";
                }
 
-               builder.append(getContentOf("index.pre.html").replace("favicon.ico",
-                               favicon));
+               builder.append(
+                               getContentOf("index.pre.html").replace("favicon.ico", favicon));
 
                if (banner) {
                        builder.append("<div class='banner'>\n");
@@ -1143,6 +1160,24 @@ 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");
+       }
+
        public static void main(String[] args) throws IOException {
                Instance.init();
                WebLibraryServer web = new WebLibraryServer(false);
index e00a52ac343ca57486addaee999f0c07402b62a0..c84c36dcb1fc89a4b36e7a835c5d5f93c265cc87 100644 (file)
@@ -1,3 +1,15 @@
+/*
+ * #AAA391: main gray
+ * #555C7E: reverse main
+ *
+ * #ddffdd: background pastel green
+ * #88dd88: for border
+ *
+ * #ffdddd: pastel red
+ * #dd8888: for border
+ *
+ */
+
 html, body, .main {
        margin: 0;
        padding: 0;
@@ -65,7 +77,7 @@ h3 {
        margin: auto;
        margin-top: calc(50% - 320px);
        flex-direction: column;
-       border: 1px solid gray;
+       border: 1px solid #AAA391;
        padding: 20px;
        border-radius: 10px;
 }
@@ -83,19 +95,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='clear'] {
+.filter input[name='optionNo'] {
        background-image: url('/clear-32x32.png');
        color: transparent;
        width: 36px;
-       height: 36px;
 }
 
 .books {
@@ -120,7 +152,7 @@ h3 {
 }
 
 .book_line .luid {
-       color: gray;
+       color: #AAA391;
        padding-right: 10px;
        padding-left: 10px;
 }
@@ -171,7 +203,7 @@ h3 {
        pointer-events: none;
        background-color: #aaa;
        filter: grayscale(100%);
-       border-color: gray;
+       border-color: #AAA391;
 }
 
 .bar .button:hover {
@@ -193,6 +225,13 @@ h3 {
        padding-bottom: 64px;
 }
 
+.viewer .title {
+       background-color: #AAA391;
+       text-align: center;
+       font-variant: small-caps;
+       color: white;
+}
+
 .viewer.img {
        margin: auto;
        display: block;
@@ -209,10 +248,23 @@ a.viewer.link:hover {
 
 .desc {
        display: flex;
+       padding-top: 10px;
 }
 
 .desc .cover img {
        width: 100px;
+       padding-right: 10px;
+       max-height: 280px;
+}
+
+.desc .details .col1 {
+       font-weight: bold;
+       padding-left: 5px;
+       padding-right: 5px; 
+}
+
+.desc .details .col2 {
+       color: #AAA391;
 }
 
 @media(min-width: 800px) {