weblib: use templates for all html
[fanfix.git] / src / be / nikiroo / fanfix / library / WebLibraryServerHtml.java
index e96edef95584b5f18a63f62ab630c75abe92f6b0..d5ba533f27b5ac746c631ccb2457ea3c3aa3649f 100644 (file)
@@ -5,6 +5,8 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -14,7 +16,6 @@ import javax.net.ssl.SSLServerSocketFactory;
 
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.bundles.Config;
-import be.nikiroo.fanfix.bundles.UiConfig;
 import be.nikiroo.fanfix.data.Chapter;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Paragraph;
@@ -22,6 +23,7 @@ import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.library.WebLibraryServer.WLoginResult;
 import be.nikiroo.fanfix.library.web.WebLibraryServerIndex;
+import be.nikiroo.fanfix.library.web.templates.WebLibraryServerTemplates;
 import be.nikiroo.fanfix.reader.TextOutput;
 import be.nikiroo.utils.IOUtils;
 import be.nikiroo.utils.NanoHTTPD;
@@ -35,6 +37,9 @@ abstract class WebLibraryServerHtml implements Runnable {
        private NanoHTTPD server;
        protected TraceHandler tracer = new TraceHandler();
 
+       WebLibraryServerTemplates templates = WebLibraryServerTemplates
+                       .getInstance();
+
        abstract protected WLoginResult login(String who, String cookie);
 
        abstract protected WLoginResult login(String who, String key,
@@ -70,6 +75,13 @@ abstract class WebLibraryServerHtml implements Runnable {
        protected abstract Response delete(String uri, WLoginResult login)
                        throws IOException;
 
+       /**
+        * Wait until all operations are done and stop the server.
+        * <p>
+        * All the new R/W operations will be refused after a call to stop.
+        */
+       protected abstract Response stop(WLoginResult login);
+
        public WebLibraryServerHtml(boolean secure) throws IOException {
                Integer port = Instance.getInstance().getConfig()
                                .getInteger(Config.SERVER_PORT);
@@ -166,13 +178,14 @@ abstract class WebLibraryServerHtml implements Runnable {
                                }
 
                                Response rep = null;
-                               if (!login.isSuccess() && WebLibraryUrls.isSupportedUrl(uri)) {
-                                       rep = loginPage(login, uri);
-                               }
+                               try {
+                                       if (!login.isSuccess()
+                                                       && WebLibraryUrls.isSupportedUrl(uri, true)) {
+                                               rep = loginPage(login, uri);
+                                       }
 
-                               if (rep == null) {
-                                       try {
-                                               if (WebLibraryUrls.isSupportedUrl(uri)) {
+                                       if (rep == null) {
+                                               if (WebLibraryUrls.isSupportedUrl(uri, false)) {
                                                        if (WebLibraryUrls.INDEX_URL.equals(uri)) {
                                                                rep = root(session, cookies, login);
                                                        } else if (WebLibraryUrls.VERSION_URL.equals(uri)) {
@@ -210,6 +223,8 @@ abstract class WebLibraryServerHtml implements Runnable {
                                                                }
                                                        } else if (WebLibraryUrls.isDeleteUrl(uri)) {
                                                                rep = delete(uri, login);
+                                                       } else if (WebLibraryUrls.EXIT_URL.equals(uri)) {
+                                                               rep = WebLibraryServerHtml.this.stop(login);
                                                        } else {
                                                                getTraceHandler().error(
                                                                                "Supported URL was not processed: "
@@ -243,22 +258,19 @@ abstract class WebLibraryServerHtml implements Runnable {
                                                                                NanoHTTPD.MIME_PLAINTEXT, "Not Found");
                                                        }
                                                }
-                                       } catch (Exception e) {
-                                               Instance.getInstance().getTraceHandler().error(
-                                                               new IOException("Cannot process web request",
-                                                                               e));
-                                               rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
-                                                               NanoHTTPD.MIME_PLAINTEXT, "An error occured");
                                        }
+                               } catch (Exception e) {
+                                       Instance.getInstance().getTraceHandler().error(
+                                                       new IOException("Cannot process web request", e));
+                                       rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
+                                                       NanoHTTPD.MIME_PLAINTEXT, "An error occured");
                                }
 
                                return rep;
                        }
                };
 
-               if (ssf != null)
-
-               {
+               if (ssf != null) {
                        getTraceHandler().trace("Install SSL on the web server...");
                        server.makeSecure(ssf, null);
                        getTraceHandler().trace("Done.");
@@ -274,6 +286,10 @@ abstract class WebLibraryServerHtml implements Runnable {
                }
        }
 
+       protected void doStop() {
+               server.stop();
+       }
+
        /**
         * The traces handler for this {@link WebLibraryServerHtml}.
         * 
@@ -297,43 +313,26 @@ abstract class WebLibraryServerHtml implements Runnable {
                this.tracer = tracer;
        }
 
-       private Response loginPage(WLoginResult login, String uri) {
-               StringBuilder builder = new StringBuilder();
-
-               appendPreHtml(builder, true);
+       private Response loginPage(WLoginResult login, String uri)
+                       throws IOException {
+               List<Template> content = new ArrayList<Template>();
 
                if (login.isBadLogin()) {
-                       builder.append("<div class='error'>Bad login or password</div>");
+                       content.add(templates.message("Bad login or password", true));
                } else if (login.isBadCookie()) {
-                       builder.append("<div class='error'>Your session timed out</div>");
-               }
-
-               if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
-                       uri = WebLibraryUrls.INDEX_URL;
+                       content.add(templates.message("Your session timed out", true));
                }
 
-               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");
-               builder.append("</form>\n");
+               content.add(templates.login(uri));
 
-               appendPostHtml(builder);
-
-               return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
-                               NanoHTTPD.MIME_HTML, builder.toString());
+               return NanoHTTPD.newChunkedResponse(Status.FORBIDDEN,
+                               NanoHTTPD.MIME_HTML, templates.index(true, content).read());
        }
 
        private Response root(IHTTPSession session, Map<String, String> cookies,
                        WLoginResult login) throws IOException {
                BasicLibrary lib = Instance.getInstance().getLibrary();
                MetaResultList result = new MetaResultList(metas(login));
-               StringBuilder builder = new StringBuilder();
-
-               appendPreHtml(builder, true);
 
                Map<String, String> params = session.getParms();
 
@@ -357,45 +356,49 @@ abstract class WebLibraryServerHtml implements Runnable {
 
                // TODO: javascript in realtime, using visible=false + hide [submit]
 
-               builder.append("<form class='browser'>\n");
-               builder.append("<div class='breadcrumbs'>\n");
-
-               builder.append("\t<select name='browser'>");
-               appendOption(builder, 2, "", "", browser);
-               appendOption(builder, 2, "Sources", "sources", browser);
-               appendOption(builder, 2, "Authors", "authors", browser);
-               appendOption(builder, 2, "Tags", "tags", browser);
-               builder.append("\t</select>\n");
+               List<Template> selects = new ArrayList<Template>();
+               boolean sourcesSel = false;
+               boolean authorsSel = false;
+               boolean tagsSel = false;
 
                if (!browser.isEmpty()) {
-                       builder.append("\t<select name='browser2'>");
+                       List<Template> options = new ArrayList<Template>();
+
                        if (browser.equals("sources")) {
+                               sourcesSel = true;
                                filterSource = browser2.isEmpty() ? filterSource : browser2;
+
                                // TODO: if 1 group -> no group
-                               appendOption(builder, 2, "", "", browser2);
                                Map<String, List<String>> sources = result.getSourcesGrouped();
                                for (String source : sources.keySet()) {
-                                       appendOption(builder, 2, source, source, browser2);
+                                       options.add(
+                                                       templates.browserOption(source, source, browser2));
                                }
                        } else if (browser.equals("authors")) {
+                               authorsSel = true;
                                filterAuthor = browser2.isEmpty() ? filterAuthor : browser2;
+
                                // TODO: if 1 group -> no group
-                               appendOption(builder, 2, "", "", browser2);
                                Map<String, List<String>> authors = result.getAuthorsGrouped();
                                for (String author : authors.keySet()) {
-                                       appendOption(builder, 2, author, author, browser2);
+                                       options.add(
+                                                       templates.browserOption(author, author, browser2));
                                }
                        } else if (browser.equals("tags")) {
+                               tagsSel = true;
                                filterTag = browser2.isEmpty() ? filterTag : browser2;
-                               appendOption(builder, 2, "", "", browser2);
+
                                for (String tag : result.getTags()) {
-                                       appendOption(builder, 2, tag, tag, browser2);
+                                       options.add(templates.browserOption(tag, tag, browser2));
                                }
                        }
-                       builder.append("\t</select>\n");
+
+                       selects.add(templates.browserSelect("browser2", browser2, options));
                }
 
                if (!browser2.isEmpty()) {
+                       List<Template> options = new ArrayList<Template>();
+
                        if (browser.equals("sources")) {
                                filterSource = browser3.isEmpty() ? filterSource : browser3;
                                Map<String, List<String>> sourcesGrouped = result
@@ -403,12 +406,10 @@ abstract class WebLibraryServerHtml implements Runnable {
                                List<String> sources = sourcesGrouped.get(browser2);
                                if (sources != null && !sources.isEmpty()) {
                                        // TODO: single empty value
-                                       builder.append("\t<select name='browser3'>");
-                                       appendOption(builder, 2, "", "", browser3);
                                        for (String source : sources) {
-                                               appendOption(builder, 2, source, source, browser3);
+                                               options.add(templates.browserOption(source, source,
+                                                               browser3));
                                        }
-                                       builder.append("\t</select>\n");
                                }
                        } else if (browser.equals("authors")) {
                                filterAuthor = browser3.isEmpty() ? filterAuthor : browser3;
@@ -417,33 +418,17 @@ abstract class WebLibraryServerHtml implements Runnable {
                                List<String> authors = authorsGrouped.get(browser2);
                                if (authors != null && !authors.isEmpty()) {
                                        // TODO: single empty value
-                                       builder.append("\t<select name='browser3'>");
-                                       appendOption(builder, 2, "", "", browser3);
                                        for (String author : authors) {
-                                               appendOption(builder, 2, author, author, browser3);
+                                               options.add(templates.browserOption(author, author,
+                                                               browser3));
                                        }
-                                       builder.append("\t</select>\n");
                                }
                        }
-               }
 
-               builder.append("\t<input type='submit' value='Select'/>\n");
-               builder.append("</div>\n");
+                       selects.add(templates.browserSelect("browser3", browser3, options));
+               }
 
-               // TODO: javascript in realtime, using visible=false + hide [submit]
-               builder.append("<div class='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='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'>");
+               List<Template> booklines = new ArrayList<Template>();
                for (MetaData meta : result.getMetas()) {
                        if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
                                        .contains(filter.toLowerCase())) {
@@ -466,39 +451,25 @@ abstract class WebLibraryServerHtml implements Runnable {
                                continue;
                        }
 
-                       builder.append("<div class='book_line'>");
-                       builder.append("<a href='");
-                       builder.append(
-                                       WebLibraryUrls.getViewUrl(meta.getLuid(), null, null));
-                       builder.append("'");
-                       builder.append(" class='link'>");
-
-                       if (lib.isCached(meta.getLuid())) {
-                               // â—‰ = &#9673;
-                               builder.append(
-                                               "<span class='cache_icon cached'>&#9673;</span>");
-                       } else {
-                               // â—‹ = &#9675;
-                               builder.append(
-                                               "<span class='cache_icon uncached'>&#9675;</span>");
-                       }
-                       builder.append("<span class='luid'>");
-                       builder.append(meta.getLuid());
-                       builder.append("</span>");
-                       builder.append("<span class='title'>");
-                       builder.append(meta.getTitle());
-                       builder.append("</span>");
-                       builder.append("<span class='author'>");
+                       String author = "";
                        if (meta.getAuthor() != null && !meta.getAuthor().isEmpty()) {
-                               builder.append("(").append(meta.getAuthor()).append(")");
+                               author = "(" + meta.getAuthor() + ")";
                        }
-                       builder.append("</span>");
-                       builder.append("</a></div>\n");
+
+                       booklines.add(templates.bookline( //
+                                       meta.getLuid(), //
+                                       WebLibraryUrls.getViewUrl(meta.getLuid(), null, null), //
+                                       meta.getTitle(), //
+                                       author, //
+                                       lib.isCached(meta.getLuid()) //
+                       ));
                }
-               builder.append("</div>");
 
-               appendPostHtml(builder);
-               return NanoHTTPD.newFixedLengthResponse(builder.toString());
+               // Add the browser in front of the booklines
+               booklines.add(0, templates.browser(browser, filter, selects));
+
+               return newInputStreamResponse(NanoHTTPD.MIME_HTML,
+                               templates.index(true, booklines).read());
        }
 
        private Response getViewer(Map<String, String> cookies, String uri,
@@ -551,9 +522,6 @@ abstract class WebLibraryServerHtml implements Runnable {
                                                NanoHTTPD.MIME_PLAINTEXT, "Story not found");
                        }
 
-                       StringBuilder builder = new StringBuilder();
-                       appendPreHtml(builder, false);
-
                        // For images documents, always go to the images if not chap 0 desc
                        if (story.getMeta().isImageDocument()) {
                                if (chapter > 0 && paragraph <= 0)
@@ -574,14 +542,13 @@ abstract class WebLibraryServerHtml implements Runnable {
 
                        String first, previous, next, last;
 
-                       StringBuilder content = new StringBuilder();
-
-                       String disabledLeft = "";
-                       String disabledRight = "";
-                       String disabledZoomReal = "";
-                       String disabledZoomWidth = "";
-                       String disabledZoomHeight = "";
+                       boolean disabledLeft = false;
+                       boolean disabledRight = false;
+                       boolean disabledZoomReal = false;
+                       boolean disabledZoomWidth = false;
+                       boolean disabledZoomHeight = false;
 
+                       Template viewerItem = null;
                        if (paragraph <= 0) {
                                first = WebLibraryUrls.getViewUrl(luid, 0, null);
                                previous = WebLibraryUrls.getViewUrl(luid,
@@ -592,40 +559,37 @@ abstract class WebLibraryServerHtml implements Runnable {
                                last = WebLibraryUrls.getViewUrl(luid,
                                                story.getChapters().size(), null);
 
-                               StringBuilder desc = new StringBuilder();
-
+                               Template desc = null;
                                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<a href='" + next + "' class='cover'>\n");
-                                       desc.append("\t\t<img src='/story/" + luid + "/cover'/>\n");
-                                       desc.append("\t</a>\n");
-                                       desc.append("\t<table class='details'>\n");
+                                       List<Template> desclines = new ArrayList<Template>();
                                        Map<String, String> details = BasicLibrary
                                                        .getMetaDesc(story.getMeta());
                                        for (String key : details.keySet()) {
-                                               appendTableRow(desc, 2, key, details.get(key));
+                                               desclines.add(templates.viewerDescline(key,
+                                                               details.get(key)));
                                        }
-                                       desc.append("\t</table>\n");
-                                       desc.append("</div>\n");
-                                       desc.append("<h1 class='title'>Description</h1>\n");
+
+                                       desc = templates.viewerDesc( //
+                                                       story.getMeta().getTitle(), //
+                                                       next, //
+                                                       WebLibraryUrls.getStoryUrlCover(luid), //
+                                                       desclines //
+                                       );
+                               }
+
+                               String content;
+                               if (chap.getParagraphs().size() <= 0) {
+                                       content = "No content provided.";
+                               } else {
+                                       content = new TextOutput(false).convert(chap, chapter > 0);
                                }
 
-                               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");
+                               viewerItem = templates.viewerText(desc, content);
 
                                if (chapter <= 0)
-                                       disabledLeft = " disabled='disbaled'";
+                                       disabledLeft = true;
                                if (chapter >= story.getChapters().size())
-                                       disabledRight = " disabled='disbaled'";
+                                       disabledRight = true;
                        } else {
                                first = WebLibraryUrls.getViewUrl(luid, chapter, 1);
                                previous = WebLibraryUrls.getViewUrl(luid, chapter,
@@ -636,13 +600,13 @@ abstract class WebLibraryServerHtml implements Runnable {
                                                chap.getParagraphs().size());
 
                                if (paragraph <= 1)
-                                       disabledLeft = " disabled='disbaled'";
+                                       disabledLeft = true;
                                if (paragraph >= chap.getParagraphs().size())
-                                       disabledRight = " disabled='disbaled'";
+                                       disabledRight = true;
 
                                // First -> previous *chapter*
                                if (chapter > 0)
-                                       disabledLeft = "";
+                                       disabledLeft = false;
                                first = WebLibraryUrls.getViewUrl(luid,
                                                (Math.max(chapter - 1, 0)), null);
                                if (paragraph <= 1) {
@@ -660,61 +624,47 @@ abstract class WebLibraryServerHtml implements Runnable {
 
                                if (para.getType() == ParagraphType.IMAGE) {
                                        String zoomStyle = "max-width: 100%;";
-                                       disabledZoomWidth = " disabled='disabled'";
+                                       disabledZoomWidth = true;
                                        String zoomOption = cookies.get("zoom");
                                        if (zoomOption != null && !zoomOption.isEmpty()) {
                                                if (zoomOption.equals("real")) {
                                                        zoomStyle = "";
-                                                       disabledZoomWidth = "";
-                                                       disabledZoomReal = " disabled='disabled'";
+                                                       disabledZoomWidth = false;
+                                                       disabledZoomReal = true;
                                                } 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'";
+                                                       disabledZoomWidth = false;
+                                                       disabledZoomHeight = true;
                                                }
                                        }
 
-                                       String javascript = "document.getElementById(\"previous\").click(); return false;";
-                                       content.append(String.format("" //
-                                                       + "<a class='viewer link' oncontextmenu='%s' href='%s'>"
-                                                       + "<img class='viewer img' style='%s' src='%s'/>"
-                                                       + "</a>", //
-                                                       javascript, //
-                                                       next, //
-                                                       zoomStyle, //
-                                                       WebLibraryUrls.getStoryUrl(luid, chapter,
-                                                                       paragraph)));
+                                       viewerItem = templates.viewerImage(WebLibraryUrls
+                                                       .getStoryUrl(luid, chapter, paragraph), next,
+                                                       zoomStyle);
                                } else {
-                                       content.append(String.format("" //
-                                                       + "<div class='viewer text'>%s</div>", //
-                                                       para.getContent()));
+                                       viewerItem = templates.viewerText(null,
+                                                       new TextOutput(false).convert(para));
                                }
                        }
 
-                       builder.append(String.format("" //
-                                       + "<div class='bar navbar'>\n" //
-                                       + "\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
-                                       + "\t<a%s id='previous' 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
+                       // List of chap/para links for navbar
 
-                       appendItemA(builder, 3, WebLibraryUrls.getViewUrl(luid, 0, null),
-                                       "Description", paragraph == 0 && chapter == 0);
+                       List<Template> links = new ArrayList<Template>();
+                       links.add(templates.viewerLink( //
+                                       "Description", //
+                                       WebLibraryUrls.getViewUrl(luid, 0, null), //
+                                       paragraph == 0 && chapter == 0 //
+                       ));
                        if (paragraph > 0) {
                                for (int i = 1; i <= chap.getParagraphs().size(); i++) {
-                                       appendItemA(builder, 3,
-                                                       WebLibraryUrls.getViewUrl(luid, chapter, i),
-                                                       "Image " + i, paragraph == i);
+                                       links.add(templates.viewerLink( //
+                                                       "Image " + i, //
+                                                       WebLibraryUrls.getViewUrl(luid, chapter, i), //
+                                                       paragraph == i //
+                                       ));
                                }
                        } else {
                                int i = 1;
@@ -724,53 +674,63 @@ abstract class WebLibraryServerHtml implements Runnable {
                                                chapName += ": " + c.getName();
                                        }
 
-                                       appendItemA(builder, 3,
-                                                       WebLibraryUrls.getViewUrl(luid, i, null), chapName,
-                                                       chapter == i);
+                                       links.add(templates.viewerLink( //
+                                                       chapName, //
+                                                       WebLibraryUrls.getViewUrl(luid, i, null), //
+                                                       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 ");
+                       // Navbar
+
+                       Template navbar = templates.viewerNavbar( //
+                                       paragraph > 0 ? paragraph : chapter, //
+                                       links, //
+                                       first, //
+                                       previous, //
+                                       next, //
+                                       last, //
+                                       disabledLeft, //
+                                       disabledLeft, //
+                                       disabledRight, //
+                                       disabledRight //
+                       );
+
+                       // Buttons on the optionbar
+
+                       List<Template> buttons = new ArrayList<Template>();
+                       buttons.add(templates.viewerOptionbarButton( //
+                                       "Back", "/", "back", false));
                        if (paragraph > 0) {
-                               builder.append("s4");
-                       } else {
-                               builder.append("s1");
+                               buttons.add(templates.viewerOptionbarButton( //
+                                               "1:1", uri + "?optionName=zoom&optionValue=real",
+                                               "zoomreal", disabledZoomReal));
+                               buttons.add(templates.viewerOptionbarButton( //
+                                               "Width", uri + "?optionName=zoom&optionValue=width",
+                                               "zoomwidth", disabledZoomWidth));
+                               buttons.add(templates.viewerOptionbarButton( //
+                                               "Height", uri + "?optionName=zoom&optionValue=height",
+                                               "zoomHeight", disabledZoomHeight));
                        }
-                       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" //
-                               ));
-                       }
+                       // Optionbar
 
-                       appendPostHtml(builder);
-                       return NanoHTTPD.newFixedLengthResponse(Status.OK,
-                                       NanoHTTPD.MIME_HTML, builder.toString());
+                       Template optionbar = templates.viewerOptionbar( //
+                                       (paragraph > 0) ? 4 : 1, //
+                                       buttons //
+                       );
+
+                       // Full content
+
+                       return newInputStreamResponse(NanoHTTPD.MIME_HTML, //
+                                       templates.index(false, Arrays.asList( //
+                                                       navbar, //
+                                                       viewerItem, //
+                                                       optionbar //
+                                       )).read());
                } catch (IOException e) {
                        Instance.getInstance().getTraceHandler()
                                        .error(new IOException("Cannot get image: " + uri, e));
@@ -786,94 +746,4 @@ abstract class WebLibraryServerHtml implements Runnable {
                }
                return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
        }
-
-       private String getContentOf(String 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));
-                       }
-               }
-
-               return "";
-       }
-
-       private void appendPreHtml(StringBuilder builder, boolean banner) {
-               String favicon = "favicon.ico";
-               String icon = Instance.getInstance().getUiConfig()
-                               .getString(UiConfig.PROGRAM_ICON);
-               if (icon != null) {
-                       favicon = "icon_" + icon.replace("-", "_") + ".png";
-               }
-
-               builder.append(
-                               getContentOf("index.pre.html").replace("favicon.ico", favicon));
-
-               if (banner) {
-                       builder.append("<div class='banner'>\n");
-                       builder.append("\t<img class='ico' src='/") //
-                                       .append(favicon) //
-                                       .append("'/>\n");
-                       builder.append("\t<h1>Fanfix</h1>\n");
-                       builder.append("\t<h2>") //
-                                       .append(Version.getCurrentVersion()) //
-                                       .append("</h2>\n");
-                       builder.append("</div>\n");
-               }
-       }
-
-       private void appendPostHtml(StringBuilder builder) {
-               builder.append(getContentOf("index.post.html"));
-       }
-
-       private void appendOption(StringBuilder builder, int depth, String name,
-                       String value, String selected) {
-               for (int i = 0; i < depth; i++) {
-                       builder.append("\t");
-               }
-               builder.append("<option value='").append(value).append("'");
-               if (value.equals(selected)) {
-                       builder.append(" selected='selected'");
-               }
-               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");
-       }
 }