Merge branch 'master' into subtree
authorNiki Roo <niki@nikiroo.be>
Wed, 20 May 2020 12:18:46 +0000 (14:18 +0200)
committerNiki Roo <niki@nikiroo.be>
Wed, 20 May 2020 12:18:46 +0000 (14:18 +0200)
20 files changed:
1  2 
data/MetaData.java
library/Template.java
library/WebLibraryServerHtml.java
library/web/style.css
library/web/templates/WebLibraryServerTemplates.java
library/web/templates/index.html
library/web/templates/login.html
library/web/templates/message.html
library/web/templates/viewer.html
supported/BasicSupport.java
supported/BasicSupport_Deprecated.java
supported/E621.java
supported/EHentai.java
supported/Fanfiction.java
supported/Fimfiction.java
supported/FimfictionApi.java
supported/MangaHub.java
supported/MangaLel.java
supported/Text.java
supported/YiffStar.java

diff --combined data/MetaData.java
index 2e34ef98ad27bda10ff34a009fa3d3d314aaf6b2,1c6ad42838d5541dbd69533b9fb1e2d21d52dd60..1c6ad42838d5541dbd69533b9fb1e2d21d52dd60
@@@ -4,6 -4,7 +4,7 @@@ import java.io.Serializable
  import java.util.ArrayList;
  import java.util.List;
  
+ import be.nikiroo.fanfix.supported.SupportType;
  import be.nikiroo.utils.Image;
  import be.nikiroo.utils.StringUtils;
  
@@@ -85,7 -86,7 +86,7 @@@ public class MetaData implements Clonea
        }
  
        /**
-        * The story publication date.
+        * The story publication date, we try to use "YYYY-mm-dd" when possible.
         * 
         * @return the date
         */
@@@ -94,7 -95,7 +95,7 @@@
        }
  
        /**
-        * The story publication date.
+        * The story publication date, we try to use "YYYY-mm-dd" when possible.
         * 
         * @param date
         *            the date to set
        }
  
        /**
-        * The cover image of the story if any (can be NULL).
+        * The cover image of the story, if any (can be NULL).
         * <p>
         * The cover is not fetched until the story is.
         * 
        }
  
        /**
-        * The cover image of the story if any (can be NULL).
+        * The cover image of the story, if any (can be NULL).
         * <p>
         * The cover is not fetched until the story is.
         * 
        }
  
        /**
-        * The subject of the story (or instance, if it is a fanfiction, what is the
+        * The subject of the story (for instance, if it is a fanfiction, what is the
         * original work; if it is a technical text, what is the technical
         * subject...).
         * 
        }
  
        /**
-        * The source of this story (which online library it was downloaded from).
+        * The source of this story -- a very user-visible piece of data.
+        * <p>
+        * It is initialised with the same value as {@link MetaData#getPublisher()},
+        * but the user is allowed to change it into any value -- this is a sort of
+        * 'category'.
         * 
         * @return the source
         */
        }
  
        /**
-        * The source of this story (which online library it was downloaded from).
+        * The source of this story -- a very user-visible piece of data.
+        * <p>
+        * It is initialised with the same value as {@link MetaData#getPublisher()},
+        * but the user is allowed to change it into any value -- this is a sort of
+        * 'category'.
         * 
         * @param source
         *            the source to set
        }
  
        /**
-        * A unique value representing the story in the local library.
+        * A unique value representing the story in the local library (usually a
+        * numerical value 0-padded with a minimum size of 3; but this is subject to
+        * change and you can also obviously have more than 1000 stories --
+        * <strong>a luid may potentially be anything else, including non-numeric
+        * characters</strong>).
+        * <p>
+        * A NULL or empty luid represents an incomplete, corrupted or fake
+        * {@link Story}.
         * 
         * @return the luid
         */
        }
  
        /**
-        * A unique value representing the story in the local library.
+        * A unique value representing the story in the local library (usually a
+        * numerical value 0-padded with a minimum size of 3; but this is subject to
+        * change and you can also obviously have more than 1000 stories --
+        * <strong>a luid may potentially be anything else, including non-numeric
+        * characters</strong>).
+        * <p>
+        * A NULL or empty luid represents an incomplete, corrupted or fake
+        * {@link Story}.
         * 
         * @param luid
         *            the luid to set
        }
  
        /**
-        * The story publisher (other the same as the source).
+        * The story publisher -- which is also the user representation of the
+        * output type this {@link Story} is in (see {@link SupportType}).
+        * <p>
+        * It allows you to know where the {@link Story} comes from, and is not
+        * supposed to change.
+        * <p>
+        * It's the user representation of the enum
+        * ({@link SupportType#getSourceName()}, not
+        * {@link SupportType#toString()}).
         * 
         * @return the publisher
         */
        }
  
        /**
-        * The story publisher (other the same as the source).
+        * The story publisher -- which is also the user representation of the
+        * output type this {@link Story} is in (see {@link SupportType}).
+        * <p>
+        * It allows you to know where the {@link Story} comes from, and is not
+        * supposed to change.
+        * <p>
+        * It's the user representation of the enum
+        * ({@link SupportType#getSourceName()}, not
+        * {@link SupportType#toString()}).
         * 
         * @param publisher
         *            the publisher to set
        }
  
        /**
-        * The output type this {@link Story} is in.
+        * The output type this {@link Story} is in (see {@link SupportType}).
+        * <p>
+        * It allows you to know where the {@link Story} comes from, and is not
+        * supposed to change.
+        * <p>
+        * It's the direct representation of the enum
+        * ({@link SupportType#toString()}, not
+        * {@link SupportType#getSourceName()}).
         * 
         * @return the type the type
         */
        }
  
        /**
-        * The output type this {@link Story} is in.
+        * The output type this {@link Story} is in (see {@link SupportType}).
+        * <p>
+        * It allows you to know where the {@link Story} comes from, and is not
+        * supposed to change.
+        * <p>
+        * It's the direct representation of the enum
+        * ({@link SupportType#toString()}, not
+        * {@link SupportType#getSourceName()}).
         * 
         * @param type
         *            the new type to set
  
        /**
         * Document catering mostly to image files.
+        * <p>
+        * I.E., this is a comics or a manga, not a textual story with actual words.
+        * <p>
+        * In image documents, all the paragraphs are supposed to be images.
         * 
         * @return the imageDocument state
         */
  
        /**
         * Document catering mostly to image files.
+        * <p>
+        * I.E., this is a comics or a manga, not a textual story with actual words.
+        * <p>
+        * In image documents, all the paragraphs are supposed to be images.
         * 
         * @param imageDocument
         *            the imageDocument state to set
        }
  
        /**
-        * The number of words in the related {@link Story}.
+        * The number of words (or images if this is an image document -- see
+        * {@link MetaData#isImageDocument()}) in the related {@link Story}.
         * 
-        * @return the number of words
+        * @return the number of words/images
         */
        public long getWords() {
                return words;
        }
  
        /**
-        * The number of words in the related {@link Story}.
+        * The number of words (or images if this is an image document -- see
+        * {@link MetaData#isImageDocument()}) in the related {@link Story}.
         * 
         * @param words
-        *            the number of words to set
+        *            the number of words/images to set
         */
        public void setWords(long words) {
                this.words = words;
        }
  
        /**
-        * The (Fanfix) {@link Story} creation date.
+        * The (Fanfix) {@link Story} creation date, i.e., when the {@link Story}
+        * was fetched via Fanfix.
         * 
-        * @return the creationDate
+        * @return the creation date
         */
        public String getCreationDate() {
                return creationDate;
        }
  
        /**
-        * The (Fanfix) {@link Story} creation date.
+        * The (Fanfix) {@link Story} creation date, i.e., when the {@link Story}
+        * was fetched via Fanfix.
         * 
         * @param creationDate
-        *            the creationDate to set
+        *            the creation date to set
         */
        public void setCreationDate(String creationDate) {
                this.creationDate = creationDate;
        }
  
        /**
-        * The cover in this {@link MetaData} object is "fake", in the sens that it
+        * The cover in this {@link MetaData} object is "fake", in the sense that it
         * comes from the actual content images.
         * 
         * @return TRUE for a fake cover
        }
  
        /**
-        * The cover in this {@link MetaData} object is "fake", in the sens that it
+        * The cover in this {@link MetaData} object is "fake", in the sense that it
         * comes from the actual content images
         * 
         * @param fakeCover
diff --combined library/Template.java
index 0000000000000000000000000000000000000000,3536d5f12efccf7da066ce4b1d140553c633e631..3536d5f12efccf7da066ce4b1d140553c633e631
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,100 +1,100 @@@
+ package be.nikiroo.fanfix.library;
+ import java.io.IOException;
+ import java.io.InputStream;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+ import be.nikiroo.utils.IOUtils;
+ import be.nikiroo.utils.streams.ReplaceInputStream;
+ public class Template {
+       private Class<?> location;
+       private String name;
+       private Map<String, String> values = new HashMap<String, String>();
+       private Map<String, Template> valuesTemplate = new HashMap<String, Template>();
+       private Map<String, List<Template>> valuesTemplateList = new HashMap<String, List<Template>>();
+       public Template(Class<?> location, String name) {
+               this.location = location;
+               this.name = name;
+       }
+       public synchronized InputStream read() throws IOException {
+               String from[] = new String[values.size() + valuesTemplate.size()
+                               + valuesTemplateList.size()];
+               String to[] = new String[from.length];
+               int i = 0;
+               for (String key : values.keySet()) {
+                       from[i] = "${" + key + "}";
+                       to[i] = values.get(key);
+                       i++;
+               }
+               for (String key : valuesTemplate.keySet()) {
+                       InputStream value = valuesTemplate.get(key).read();
+                       try {
+                               from[i] = "${" + key + "}";
+                               to[i] = IOUtils.readSmallStream(value);
+                       } finally {
+                               value.close();
+                       }
+                       i++;
+               }
+               for (String key : valuesTemplateList.keySet()) {
+                       List<Template> templates = valuesTemplateList.get(key);
+                       StringBuilder value = new StringBuilder();
+                       for (Template template : templates) {
+                               InputStream valueOne = template.read();
+                               try {
+                                       value.append(IOUtils.readSmallStream(valueOne));
+                               } finally {
+                                       valueOne.close();
+                               }
+                       }
+                       from[i] = "${" + key + "}";
+                       to[i] = value.toString();
+                       i++;
+               }
+               
+               InputStream in = IOUtils.openResource(location, name);
+               return new ReplaceInputStream(in, from, to);
+       }
+       public synchronized Template set(String key, String value) {
+               values.put(key, value);
+               valuesTemplate.remove(key);
+               valuesTemplateList.remove(key);
+               return this;
+       }
+       public synchronized Template set(String key, Template value) {
+               values.remove(key);
+               valuesTemplate.put(key, value);
+               valuesTemplateList.remove(key);
+               return this;
+       }
+       public synchronized Template set(String key, List<Template> value) {
+               values.remove(key);
+               valuesTemplate.remove(key);
+               valuesTemplateList.put(key, value);
+               return this;
+       }
+       @Override
+       public String toString() {
+               return String.format(
+                               "[Template for %s with (%d,%d,%d) value(s) to replace]", name,
+                               values.size(), valuesTemplate.size(),
+                               valuesTemplateList.size());
+       }
+ }
index 69d8671736d7e2e4cf8c2922ca61358803f6bd2f,42ebead137c05e8189fec17a9d57d4da91e4858a..42ebead137c05e8189fec17a9d57d4da91e4858a
@@@ -5,6 -5,8 +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;
@@@ -36,6 -38,9 +38,9 @@@ abstract class WebLibraryServerHtml imp
        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,
  
        private Response loginPage(WLoginResult login, String uri)
                        throws IOException {
-               StringBuilder builder = new StringBuilder();
-               builder.append(getTemplateIndexPreBanner(true));
+               List<Template> content = new ArrayList<Template>();
  
                if (login.isBadLogin()) {
-                       builder.append(
-                                       "\t\t<div class='error'>Bad login or password</div>");
+                       content.add(templates.message("Bad login or password", true));
                } else if (login.isBadCookie()) {
-                       builder.append(
-                                       "\t\t<div class='error'>Your session timed out</div>");
+                       content.add(templates.message("Your session timed out", true));
                }
  
-               if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
-                       uri = WebLibraryUrls.INDEX_URL;
-               }
-               builder.append("\t\t<form method='POST' action='" + uri
-                               + "' class='login'>\n");
-               builder.append(
-                               "\t\t\t<p>You must be logged into the system to see the stories.</p>");
-               builder.append("\t\t\t<input type='text' name='login' />\n");
-               builder.append("\t\t\t<input type='password' name='password' />\n");
-               builder.append("\t\t\t<input type='submit' value='Login' />\n");
-               builder.append("\t\t</form>\n");
-               builder.append(getTemplate("index.post"));
+               content.add(templates.login(uri));
  
-               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();
-               builder.append(getTemplateIndexPreBanner(true));
  
                Map<String, String> params = session.getParms();
  
  
                // TODO: javascript in realtime, using visible=false + hide [submit]
  
-               StringBuilder selects = new StringBuilder();
+               List<Template> selects = new ArrayList<Template>();
                boolean sourcesSel = false;
                boolean authorsSel = false;
                boolean tagsSel = false;
  
-               String selectTemplate = getTemplate("browser.select");
                if (!browser.isEmpty()) {
-                       StringBuilder options = new StringBuilder();
+                       List<Template> options = new ArrayList<Template>();
  
                        if (browser.equals("sources")) {
                                sourcesSel = true;
                                // TODO: if 1 group -> no group
                                Map<String, List<String>> sources = result.getSourcesGrouped();
                                for (String source : sources.keySet()) {
-                                       options.append(
-                                                       getTemplateBrowserOption(source, source, browser2));
+                                       options.add(
+                                                       templates.browserOption(source, source, browser2));
                                }
                        } else if (browser.equals("authors")) {
                                authorsSel = true;
                                // TODO: if 1 group -> no group
                                Map<String, List<String>> authors = result.getAuthorsGrouped();
                                for (String author : authors.keySet()) {
-                                       options.append(
-                                                       getTemplateBrowserOption(author, author, browser2));
+                                       options.add(
+                                                       templates.browserOption(author, author, browser2));
                                }
                        } else if (browser.equals("tags")) {
                                tagsSel = true;
                                filterTag = browser2.isEmpty() ? filterTag : browser2;
  
                                for (String tag : result.getTags()) {
-                                       options.append(
-                                                       getTemplateBrowserOption(tag, tag, browser2));
+                                       options.add(templates.browserOption(tag, tag, browser2));
                                }
                        }
  
-                       selects.append(selectTemplate //
-                                       .replace("${name}", "browser2") //
-                                       .replace("${value}", browser2) //
-                                       .replace("${options}", options.toString()) //
-                       );
+                       selects.add(templates.browserSelect("browser2", browser2, options));
                }
  
                if (!browser2.isEmpty()) {
-                       StringBuilder options = new StringBuilder();
+                       List<Template> options = new ArrayList<Template>();
  
                        if (browser.equals("sources")) {
                                filterSource = browser3.isEmpty() ? filterSource : browser3;
                                if (sources != null && !sources.isEmpty()) {
                                        // TODO: single empty value
                                        for (String source : sources) {
-                                               options.append(getTemplateBrowserOption(source, source,
+                                               options.add(templates.browserOption(source, source,
                                                                browser3));
                                        }
                                }
                                if (authors != null && !authors.isEmpty()) {
                                        // TODO: single empty value
                                        for (String author : authors) {
-                                               options.append(getTemplateBrowserOption(author, author,
+                                               options.add(templates.browserOption(author, author,
                                                                browser3));
                                        }
                                }
                        }
  
-                       selects.append(selectTemplate //
-                                       .replace("${name}", "browser3") //
-                                       .replace("${value}", browser3) //
-                                       .replace("${options}", options.toString()) //
-                       );
+                       selects.add(templates.browserSelect("browser3", browser3, options));
                }
  
-               String sel = "selected='selected'";
-               builder.append(getTemplate("browser") //
-                               .replace("${sourcesSelected}", sourcesSel ? sel : "") //
-                               .replace("${authorsSelected}", authorsSel ? sel : "") //
-                               .replace("${tagsSelected}", tagsSel ? sel : "") //
-                               .replace("${filter}", filter) //
-                               .replace("${selects}", selects.toString()) //
-               );
-               builder.append("\t\t<div class='books'>\n");
+               List<Template> booklines = new ArrayList<Template>();
                for (MetaData meta : result.getMetas()) {
                        if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
                                        .contains(filter.toLowerCase())) {
                                author = "(" + meta.getAuthor() + ")";
                        }
  
-                       String cachedClass = "cached";
-                       String cached = "&#9673;";
-                       if (!lib.isCached(meta.getLuid())) {
-                               cachedClass = "uncached";
-                               cached = "&#9675;";
-                       }
-                       builder.append(getTemplate("bookline") //
-                                       .replace("${href}",
-                                                       WebLibraryUrls.getViewUrl(meta.getLuid(), null,
-                                                                       null)) //
-                                       .replace("${luid}", meta.getLuid()) //
-                                       .replace("${title}", meta.getTitle()) //
-                                       .replace("${author}", author) //
-                                       .replace("${cachedClass}", cachedClass) //
-                                       .replace("${cached}", cached) //
-                       );
+                       booklines.add(templates.bookline( //
+                                       meta.getLuid(), //
+                                       WebLibraryUrls.getViewUrl(meta.getLuid(), null, null), //
+                                       meta.getTitle(), //
+                                       author, //
+                                       lib.isCached(meta.getLuid()) //
+                       ));
                }
-               builder.append("\t\t</div>\n");
  
-               builder.append(getTemplate("index.post"));
+               // Add the browser in front of the booklines
+               booklines.add(0, templates.browser(browser, filter, selects));
  
-               return NanoHTTPD.newFixedLengthResponse(builder.toString());
+               return newInputStreamResponse(NanoHTTPD.MIME_HTML,
+                               templates.index(true, booklines).read());
        }
  
        private Response getViewer(Map<String, String> cookies, String uri,
                return html;
        }
  
-       private String getTemplateBrowserOption(String name, String value,
-                       String selected) throws IOException {
-               String selectedAttribute = "";
-               if (value.equals(selected)) {
-                       selectedAttribute = " selected='selected'";
-               }
-               return getTemplate("browser.option" //
-                               .replace("${value}", value) //
-                               .replace("${selected}", selectedAttribute) //
-                               .replace("${name}", name) //
-               );
-       }
        private String getTemplate(String template) throws IOException {
                // TODO: check if it is "slow" -> map cache
                InputStream in = IOUtils.openResource(WebLibraryServerTemplates.class,
diff --combined library/web/style.css
index b12a24fa6ea0c3018eb925200baa066d475aee75,bb91002920e94bd593d125ff8abe26c714f4c5db..bb91002920e94bd593d125ff8abe26c714f4c5db
@@@ -47,13 -47,9 +47,9 @@@ html, body, .main 
        margin: 10px;
  }
  
- .error {
+ .message.error {
        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 */
index 482a1b826e12514a0714cf5cd3256531851d4166,31abe69919acaf217e85c53c754d7a7d0b54a626..31abe69919acaf217e85c53c754d7a7d0b54a626
  package be.nikiroo.fanfix.library.web.templates;
  
+ import java.util.List;
+ import be.nikiroo.fanfix.Instance;
+ import be.nikiroo.fanfix.bundles.UiConfig;
+ import be.nikiroo.fanfix.library.Template;
+ import be.nikiroo.utils.Version;
  public class WebLibraryServerTemplates {
+       static private WebLibraryServerTemplates instance = new WebLibraryServerTemplates();
+       static public WebLibraryServerTemplates getInstance() {
+               return instance;
+       }
+       public Template bookline(String luid, String href, String title,
+                       String author, boolean cached) {
+               String cachedClass = "cached";
+               String cachedValue = "&#9673;";
+               if (!cached) {
+                       cachedClass = "uncached";
+                       cachedValue = "&#9675;";
+               }
+               return new Template(getClass(), "bookline.html") //
+                               .set("href", href) //
+                               .set("cachedClass", cachedClass) //
+                               .set("cached", cachedValue) //
+                               .set("luid", luid) //
+                               .set("title", title) //
+                               .set("author", author) //
+               ;
+       }
+       public Template index(boolean banner, List<Template> content) {
+               String favicon = "favicon.ico";
+               String icon = Instance.getInstance().getUiConfig()
+                               .getString(UiConfig.PROGRAM_ICON);
+               if (icon != null) {
+                       favicon = "icon_" + icon.replace("-", "_") + ".png";
+               }
+               Template index = new Template(getClass(), "index.html") //
+                               .set("title", "Fanfix") //
+                               .set("favicon", favicon) //
+                               .set("content", content) //
+               ;
+               if (banner) {
+                       index.set("banner", new Template(getClass(), "index.banner.html") //
+                                       .set("favicon", favicon) //
+                                       .set("version", Version.getCurrentVersion().toString()) //
+                       );
+               } else {
+                       index.set("banner", "");
+               }
+               return index;
+       }
+       public Template login(String url) {
+               return new Template(getClass(), "login.html") //
+                               .set("url", url) //
+               ;
+       }
+       public Template message(String message, boolean error) {
+               return new Template(getClass(), "message.html") //
+                               .set("class", error ? "message error" : "message") //
+                               .set("message", message) //
+               ;
+       }
+       public Template browser(String selectedValue, String filter,
+                       List<Template> selects) {
+               return new Template(getClass(), "browser.html") //
+                               .set("sourcesSelected",
+                                               "sources".equals(selectedValue) ? "selected='selected'"
+                                                               : "") //
+                               .set("authorsSelected",
+                                               "authors".equals(selectedValue) ? "selected='selected'"
+                                                               : "") //
+                               .set("tagsSelected",
+                                               "tags".equals(selectedValue) ? "selected='selected'"
+                                                               : "") //
+                               .set("filter", filter) //
+                               .set("selects", selects) //
+               ;
+       }
+       public Template browserOption(String name, String value,
+                       String selectedValue) {
+               return new Template(getClass(), "browser.option.html") //
+                               .set("value", value) //
+                               .set("selected",
+                                               value.equals(selectedValue) ? "selected='selected'"
+                                                               : "") //
+                               .set("name", name) //
+               ;
+       }
+       public Template browserSelect(String name, String value,
+                       List<Template> options) {
+               return new Template(getClass(), "browser.select.html") //
+                               .set("name", name) //
+                               .set("value", value) //
+                               .set("options", options) //
+               ;
+       }
+       public Template viewer(Template browser, List<Template> booklines) {
+               // TODO
+               return null;
+       }
+       public Template viewerImage(String src, String href, String zoomStyle) {
+               return new Template(getClass(), "viewer.image.html") //
+                               .set("src", src) //
+                               .set("href", href) //
+                               .set("zoomStyle", zoomStyle) //
+               ;
+       }
+       public Template viewerText(List<Template> desc, List<Template> content) {
+               return new Template(getClass(), "viewer.text.html") //
+                               .set("desc", desc) //
+                               .set("content", content) //
+               ;
+       }
+       public Template viewerLink(String name, String link, String className) {
+               return new Template(getClass(), "viewer.link.html") //
+                               .set("link", link) //
+                               .set("class", className) //
+                               .set("name", name) //
+               ;
+       }
+       public Template viewerNavbar(int current, List<Template> links,
+                       String hrefFirst, String hrefPrevious, String hrefNext,
+                       String hrefLast, boolean disabledFirst, boolean disabledPrevious,
+                       boolean disabledNext, boolean disabledLast) {
+               return new Template(getClass(), "viewer.navbar.html") //
+                               .set("disabledFirst",
+                                               disabledFirst ? "disabled='disabled'" : "") //
+                               .set("disabledPrevious",
+                                               disabledPrevious ? "disabled='disabled'" : "") //
+                               .set("disabledNext", disabledNext ? "disabled='disabled'" : "") //
+                               .set("disabledLast", disabledLast ? "disabled='disabled'" : "") //
+                               .set("hrefFirst", hrefFirst) //
+                               .set("hrefPrevious", hrefPrevious) //
+                               .set("hrefNext", hrefNext) //
+                               .set("hrefLast", hrefLast) //
+                               .set("current", Integer.toString(current)) //
+                               .set("links", links) //
+               ;
+       }
+       // numberOfButtons = 4 or 1 or the moment
+       public Template viewerOptionbar(int numberOfButtons,
+                       List<Template> buttons) {
+               return new Template(getClass(), "viewer.optionbar.html") //
+                               .set("classSize", "s" + numberOfButtons) //
+                               .set("buttons", buttons) //
+               ;
+       }
+       public Template viewerOptionbarButton(String value, String href,
+                       String className, boolean disabled) {
+               return new Template(getClass(), "viewer.optionbar.button.html") //
+                               .set("disabled", disabled ? "disabled='disabled'" : "") //
+                               .set("class", className) //
+                               .set("href", href) //
+                               .set("value", value) //
+               ;
+       }
  }
index 0000000000000000000000000000000000000000,6f3934e285f2d092d5054e6b013078b4ab6f41a0..6f3934e285f2d092d5054e6b013078b4ab6f41a0
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,50 +1,50 @@@
+ <!DOCTYPE html>
+ <html>
+ <head>
+       <!--
+       Copyright 2020 David ROULET
+       
+       This file is part of fanfix.
+       
+       fanfix is free software: you can redistribute it and/or modify
+       it under the terms of the GNU Affero General Public License as published by
+       the Free Software Foundation, either version 3 of the License, or
+       (at your option) any later version.
+       
+       fanfix is distributed in the hope that it will be useful,
+       but WITHOUT ANY WARRANTY; without even the implied warranty of
+       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+       GNU Affero General Public License for more details.
+       
+       You should have received a copy of the GNU Affero General Public License
+       along with fanfix.  If not, see <https://www.gnu.org/licenses/>.
+       ___________________________________________________________________________
+        This website was coded by:
+                       A kangaroo.
+                                                   _  _
+                                                  (\\( \
+                                                   `.\-.)
+                               _...._            _,-"   `-.
+ \                           ,"      `-._.- -.,-"       .  \
+  \`.                      ,"                               `.
+   \ `-...__              /                           .   .:  y
+    `._     ``-...__     /                           ,"```-._/
+       `-._         ```-"                      |    /_          //
+           `.._                   _            ;   <_ \        //
+               ``-.___             `.           `-._ \ \      //
+                      `- <           `.     (\ _/)/ `.\/     //
+                          \            \     `       ^^^^^^^^^
+       ___________________________________________________________________________
+       
+       -->
+       <meta http-equiv='content-type' content='text/html; charset=utf-8'>
+       <meta name='viewport' content='width=device-width, initial-scale=1.0'>
+       <title>${title}</title>
+       <link rel='stylesheet' type='text/css' href='/style.css' />
+       <link rel='icon' type='image/x-icon' href='/${favicon}' />
+ </head>
+ <body>
+       <div class='main'>
+ ${banner}${content}   </div>
+ </body>
index 0000000000000000000000000000000000000000,001914429438df696f33b79337229c7c0ef5ff2a..001914429438df696f33b79337229c7c0ef5ff2a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,6 +1,6 @@@
+               <form method='POST' action='${url}' class='login'>
+                       <p>You must be logged into the system to see the stories.</p>
+                       <input type='text' name='login' />
+                       <input type='password' name='password' />
+                       <input type='submit' value='Login' />
+               </form>
index 0000000000000000000000000000000000000000,a09fc4d628b439d71f6e271a485822b44ab06294..a09fc4d628b439d71f6e271a485822b44ab06294
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,1 +1,1 @@@
+               <div class='${class}'>${message}</div>
index 0000000000000000000000000000000000000000,f4b42153a21e378a8337c4629bef8b0f93eec126..f4b42153a21e378a8337c4629bef8b0f93eec126
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,1 +1,1 @@@
+ ${browser}${viewer}
index 56a3bb80cb6d51fe40a9c4d830e12cb275102d76,bcfcca1bdbb16cfd3bd8849e8fdb2bfb55a22de0..bcfcca1bdbb16cfd3bd8849e8fdb2bfb55a22de0
@@@ -272,7 -272,12 +272,12 @@@ public abstract class BasicSupport 
                pg.setProgress(30);
  
                Story story = new Story();
+               
                MetaData meta = getMeta();
+               meta.setType(getType().toString());
+               meta.setSource(getType().getSourceName());
+               meta.setPublisher(getType().getSourceName());
+               
                if (meta.getCreationDate() == null
                                || meta.getCreationDate().trim().isEmpty()) {
                        meta.setCreationDate(bsHelper
index 40ff3fc027c7e52beca3426017e1a6a58972fbbe,9aac6d5c61b713a4641fc251796b981e48d3a2ec..9aac6d5c61b713a4641fc251796b981e48d3a2ec
@@@ -203,7 -203,12 +203,12 @@@ public abstract class BasicSupport_Depr
                        pg.setProgress(30);
  
                        Story story = new Story();
+                       
                        MetaData meta = getMeta(url, getInput());
+                       meta.setType(getType().toString());
+                       meta.setSource(getType().getSourceName());
+                       meta.setPublisher(getType().getSourceName());
+                       
                        if (meta.getCreationDate() == null
                                        || meta.getCreationDate().trim().isEmpty()) {
                                meta.setCreationDate(bsHelper.formatDate(
diff --combined supported/E621.java
index adf8d28c8b2853a01fddc02b239346bec9f28a0e,f3a7238e534e7bf74ca0c1471da3cba6aca28b5e..f3a7238e534e7bf74ca0c1471da3cba6aca28b5e
@@@ -62,14 -62,11 +62,11 @@@ class E621 extends BasicSupport 
                meta.setAuthor(getAuthor());
                meta.setDate(bsHelper.formatDate(getDate()));
                meta.setTags(getTags());
-               meta.setSource(getType().getSourceName());
                meta.setUrl(getSource().toString());
-               meta.setPublisher(getType().getSourceName());
                meta.setUuid(getSource().toString());
                meta.setLuid("");
                meta.setLang("en");
                meta.setSubject("Furry");
-               meta.setType(getType().toString());
                meta.setImageDocument(true);
                meta.setCover(getCover());
                meta.setFakeCover(true);
        protected String getDesc() throws IOException {
                if (isSearchOrSet(getSource())) {
                        StringBuilder builder = new StringBuilder();
+                       builder.append("<div>");
                        builder.append("A collection of images from ")
-                                       .append(getSource().getHost()).append("\n") //
-                                       .append("\tTime of creation: "
+                                       .append(getSource().getHost()) //
+                                       .append("<br/>\n") //
+                                       .append("&nbsp;&nbsp;&nbsp;&nbsp;Time of creation: "
                                                        + StringUtils.fromTime(new Date().getTime()))
-                                       .append("\n") //
-                                       .append("\tTags: ");//
+                                       .append("<br/>\n") //
+                                       .append("&nbsp;&nbsp;&nbsp;&nbsp;tTags: ");//
                        for (String tag : getTags()) {
-                               builder.append("\t\t").append(tag);
+                               builder.append(
+                                               "\n<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;")
+                                               .append(tag);
                        }
+                       builder.append("\n</div>");
  
                        return builder.toString();
                }
@@@ -97,7 -99,7 +99,7 @@@
                if (isPool(getSource())) {
                        Element el = getSourceNode().getElementById("description");
                        if (el != null) {
-                               return el.text();
+                               return el.html();
                        }
                }
  
diff --combined supported/EHentai.java
index 3c734329e3bac03e456665958b6699cc623b3ff8,399ce913055cdb66934a287153a768fd6223f5e3..399ce913055cdb66934a287153a768fd6223f5e3
@@@ -34,14 -34,11 +34,11 @@@ class EHentai extends BasicSupport_Depr
                meta.setAuthor(getAuthor(reset(in)));
                meta.setDate(getDate(reset(in)));
                meta.setTags(getTags(reset(in)));
-               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang(getLang(reset(in)));
                meta.setSubject("Hentai");
-               meta.setType(getType().toString());
                meta.setImageDocument(true);
                meta.setCover(getCover(source, reset(in)));
                meta.setFakeCover(true);
index 16b44f806de8b1df604a0f5e0637a017f39d8ce2,060705cadd09ea0867c1152245a0bd90697fac6f..060705cadd09ea0867c1152245a0bd90697fac6f
@@@ -40,14 -40,11 +40,11 @@@ class Fanfiction extends BasicSupport_D
                meta.setAuthor(getAuthor(reset(in)));
                meta.setDate(getDate(reset(in)));
                meta.setTags(getTags(reset(in)));
-               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang("en"); // TODO find language of book
                meta.setSubject(getSubject(reset(in)));
-               meta.setType(getType().toString());
                meta.setImageDocument(false);
                meta.setCover(getCover(source, reset(in)));
  
index 77267c1188a24438bec7f46c1097219f0d20e58c,b5df2618ce671ddaf04d7f5b644c3110c721339b..b5df2618ce671ddaf04d7f5b644c3110c721339b
@@@ -38,14 -38,11 +38,11 @@@ class Fimfiction extends BasicSupport_D
                meta.setAuthor(getAuthor(reset(in)));
                meta.setDate(getDate(reset(in)));
                meta.setTags(getTags(reset(in)));
-               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang("en");
                meta.setSubject("MLP");
-               meta.setType(getType().toString());
                meta.setImageDocument(false);
                meta.setCover(getCover(reset(in)));
  
index e6dd6118721b93aec36c0d7c734faad1c6d9a95b,cdb132147679b3a912e662a112cd988273382cc0..cdb132147679b3a912e662a112cd988273382cc0
@@@ -127,14 -127,11 +127,11 @@@ class FimfictionApi extends BasicSuppor
                meta.setDate(bsHelper.formatDate(
                                getKeyJson(json, 0, "type", "story", "date_published")));
                meta.setTags(getTags());
-               meta.setSource(getType().getSourceName());
                meta.setUrl(getSource().toString());
-               meta.setPublisher(getType().getSourceName());
                meta.setUuid(getSource().toString());
                meta.setLuid("");
                meta.setLang("en");
                meta.setSubject("MLP");
-               meta.setType(getType().toString());
                meta.setImageDocument(false);
  
                String coverImageLink = getKeyJson(json, 0, "type", "story",
diff --combined supported/MangaHub.java
index 706a59114adf62949044cf1eb52b8347d38749dc,7ba12b979e9e93ac7f2426c4a588aa99369d6fd6..7ba12b979e9e93ac7f2426c4a588aa99369d6fd6
@@@ -39,14 -39,11 +39,11 @@@ class MangaHub extends BasicSupport 
                meta.setDate("");
                meta.setAuthor(getAuthor());
                meta.setTags(getTags());
-               meta.setSource(getType().getSourceName());
                meta.setUrl(getSource().toString());
-               meta.setPublisher(getType().getSourceName());
                meta.setUuid(getSource().toString());
                meta.setLuid("");
                meta.setLang("en");
                meta.setSubject("manga");
-               meta.setType(getType().toString());
                meta.setImageDocument(true);
                meta.setCover(getCover());
  
diff --combined supported/MangaLel.java
index 5910a371aedf7272e546ec7170b508516879ced1,47efad8594a71830a33e5de698e47256c3d87616..47efad8594a71830a33e5de698e47256c3d87616
@@@ -34,14 -34,11 +34,11 @@@ class MangaLel extends BasicSupport 
                meta.setAuthor(getAuthor());
                meta.setDate(bsHelper.formatDate(getDate()));
                meta.setTags(getTags());
-               meta.setSource(getType().getSourceName());
                meta.setUrl(getSource().toString());
-               meta.setPublisher(getType().getSourceName());
                meta.setUuid(getSource().toString());
                meta.setLuid("");
                meta.setLang("fr");
                meta.setSubject("manga");
-               meta.setType(getType().toString());
                meta.setImageDocument(true);
                meta.setCover(getCover());
  
diff --combined supported/Text.java
index 71e30c3252220c76f6ae5724e9632d33e67878cf,252aca0443fbfe0f71db294c0c8c1d3a816deb72..252aca0443fbfe0f71db294c0c8c1d3a816deb72
@@@ -85,14 -85,11 +85,11 @@@ class Text extends BasicSupport 
                meta.setAuthor(getAuthor());
                meta.setDate(bsHelper.formatDate(getDate()));
                meta.setTags(new ArrayList<String>());
-               meta.setSource(getType().getSourceName());
                meta.setUrl(getSourceFile().toURI().toURL().toString());
-               meta.setPublisher("");
                meta.setUuid(getSourceFile().toString());
                meta.setLuid("");
                meta.setLang(getLang()); // default is EN
                meta.setSubject(getSourceFile().getParentFile().getName());
-               meta.setType(getType().toString());
                meta.setImageDocument(false);
                meta.setCover(getCover(getSourceFile()));
                
diff --combined supported/YiffStar.java
index 6974e9ae98a52bfde44f703cb585a63a26810033,498b7d9a611db6417b153e4b5689dc786b70d513..498b7d9a611db6417b153e4b5689dc786b70d513
@@@ -34,14 -34,11 +34,11 @@@ class YiffStar extends BasicSupport_Dep
                meta.setAuthor(getAuthor(reset(in)));
                meta.setDate("");
                meta.setTags(getTags(reset(in)));
-               meta.setSource(getType().getSourceName());
                meta.setUrl(source.toString());
-               meta.setPublisher(getType().getSourceName());
                meta.setUuid(source.toString());
                meta.setLuid("");
                meta.setLang("en");
                meta.setSubject("Furry");
-               meta.setType(getType().toString());
                meta.setImageDocument(false);
                meta.setCover(getCover(source, reset(in)));