weblib: set custom covers
[fanfix.git] / src / be / nikiroo / fanfix / library / WebLibraryServerHtml.java
CommitLineData
33d40b9f
NR
1package be.nikiroo.fanfix.library;
2
3import java.io.File;
4import java.io.FileInputStream;
5import java.io.IOException;
6import java.io.InputStream;
7import java.security.KeyStore;
8import java.util.HashMap;
9import java.util.List;
10import java.util.Map;
11
12import javax.net.ssl.KeyManagerFactory;
13import javax.net.ssl.SSLServerSocketFactory;
14
15import be.nikiroo.fanfix.Instance;
16import be.nikiroo.fanfix.bundles.Config;
17import be.nikiroo.fanfix.bundles.UiConfig;
18import be.nikiroo.fanfix.data.Chapter;
19import be.nikiroo.fanfix.data.MetaData;
20import be.nikiroo.fanfix.data.Paragraph;
21import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
22import be.nikiroo.fanfix.data.Story;
23import be.nikiroo.fanfix.library.WebLibraryServer.WLoginResult;
24import be.nikiroo.fanfix.library.web.WebLibraryServerIndex;
25import be.nikiroo.fanfix.reader.TextOutput;
26import be.nikiroo.utils.IOUtils;
27import be.nikiroo.utils.NanoHTTPD;
28import be.nikiroo.utils.NanoHTTPD.IHTTPSession;
29import be.nikiroo.utils.NanoHTTPD.Response;
30import be.nikiroo.utils.NanoHTTPD.Response.Status;
31import be.nikiroo.utils.TraceHandler;
32import be.nikiroo.utils.Version;
33
34abstract class WebLibraryServerHtml implements Runnable {
35 private NanoHTTPD server;
36 protected TraceHandler tracer = new TraceHandler();
37
38 abstract protected WLoginResult login(String who, String cookie);
39
40 abstract protected WLoginResult login(String who, String key,
41 String subkey);
42
43 abstract protected WLoginResult login(boolean badLogin, boolean badCookie);
44
45 abstract protected Response getList(String uri, WLoginResult login)
46 throws IOException;
47
48 abstract protected Response getStoryPart(String uri, WLoginResult login);
49
6673ec59
NR
50 abstract protected Response getCover(String uri, WLoginResult login)
51 throws IOException;
52
e4b1b70c
NR
53 abstract protected Response setCover(String uri, String luid,
54 WLoginResult login) throws IOException;
55
33d40b9f
NR
56 abstract protected List<MetaData> metas(WLoginResult login)
57 throws IOException;
58
59 abstract protected Story story(String luid, WLoginResult login)
60 throws IOException;
61
62 public WebLibraryServerHtml(boolean secure) throws IOException {
63 Integer port = Instance.getInstance().getConfig()
64 .getInteger(Config.SERVER_PORT);
65 if (port == null) {
66 throw new IOException(
67 "Cannot start web server: port not specified");
68 }
69
70 SSLServerSocketFactory ssf = null;
71 if (secure) {
72 String keystorePath = Instance.getInstance().getConfig()
73 .getString(Config.SERVER_SSL_KEYSTORE, "");
74 String keystorePass = Instance.getInstance().getConfig()
75 .getString(Config.SERVER_SSL_KEYSTORE_PASS);
76
77 if (secure && keystorePath.isEmpty()) {
78 throw new IOException(
79 "Cannot start a secure web server: no keystore.jks file povided");
80 }
81
82 if (!keystorePath.isEmpty()) {
83 File keystoreFile = new File(keystorePath);
84 try {
85 KeyStore keystore = KeyStore
86 .getInstance(KeyStore.getDefaultType());
87 InputStream keystoreStream = new FileInputStream(
88 keystoreFile);
89 try {
90 keystore.load(keystoreStream,
91 keystorePass.toCharArray());
92 KeyManagerFactory keyManagerFactory = KeyManagerFactory
93 .getInstance(KeyManagerFactory
94 .getDefaultAlgorithm());
95 keyManagerFactory.init(keystore,
96 keystorePass.toCharArray());
97 ssf = NanoHTTPD.makeSSLSocketFactory(keystore,
98 keyManagerFactory);
99 } finally {
100 keystoreStream.close();
101 }
102 } catch (Exception e) {
103 throw new IOException(e.getMessage());
104 }
105 }
106 }
107
108 server = new NanoHTTPD(port) {
109 @Override
110 public Response serve(final IHTTPSession session) {
111 super.serve(session);
112
113 String query = session.getQueryParameterString(); // a=a%20b&dd=2
114 Method method = session.getMethod(); // GET, POST..
115 String uri = session.getUri(); // /home.html
116
117 // need them in real time (not just those sent by the UA)
118 Map<String, String> cookies = new HashMap<String, String>();
119 for (String cookie : session.getCookies()) {
120 cookies.put(cookie, session.getCookies().read(cookie));
121 }
122
123 WLoginResult login = null;
124 Map<String, String> params = session.getParms();
125 String who = session.getRemoteHostName()
126 + session.getRemoteIpAddress();
127 if (params.get("login") != null) {
128 login = login(who, params.get("password"),
129 params.get("login"));
130 } else {
131 String cookie = cookies.get("cookie");
132 login = login(who, cookie);
133 }
134
135 if (login.isSuccess()) {
136 // refresh cookie
137 session.getCookies().set(new Cookie("cookie",
138 login.getCookie(), "30; path=/"));
139
140 // set options
141 String optionName = params.get("optionName");
142 if (optionName != null && !optionName.isEmpty()) {
143 String optionNo = params.get("optionNo");
144 String optionValue = params.get("optionValue");
145 if (optionNo != null || optionValue == null
146 || optionValue.isEmpty()) {
147 session.getCookies().delete(optionName);
148 cookies.remove(optionName);
149 } else {
150 session.getCookies().set(new Cookie(optionName,
151 optionValue, "; path=/"));
152 cookies.put(optionName, optionValue);
153 }
154 }
155 }
156
157 Response rep = null;
158 if (!login.isSuccess() && WebLibraryUrls.isSupportedUrl(uri)) {
159 rep = loginPage(login, uri);
160 }
161
162 if (rep == null) {
163 try {
164 if (WebLibraryUrls.isSupportedUrl(uri)) {
165 if (WebLibraryUrls.INDEX_URL.equals(uri)) {
166 rep = root(session, cookies, login);
167 } else if (WebLibraryUrls.VERSION_URL.equals(uri)) {
168 rep = newFixedLengthResponse(Status.OK,
169 MIME_PLAINTEXT,
170 Version.getCurrentVersion().toString());
6673ec59 171 } else if (WebLibraryUrls.isCoverUrl(uri)) {
e4b1b70c
NR
172 String luid = params.get("luid");
173 if (luid != null) {
174 rep = setCover(uri, luid, login);
175 } else {
176 rep = getCover(uri, login);
177 }
33d40b9f
NR
178 } else if (WebLibraryUrls.isListUrl(uri)) {
179 rep = getList(uri, login);
180 } else if (WebLibraryUrls.isStoryUrl(uri)) {
181 rep = getStoryPart(uri, login);
182 } else if (WebLibraryUrls.isViewUrl(uri)) {
183 rep = getViewer(cookies, uri, login);
184 } else if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
185 session.getCookies().delete("cookie");
186 cookies.remove("cookie");
187 rep = loginPage(login(false, false), uri);
188 } else {
189 getTraceHandler().error(
190 "Supported URL was not processed: "
191 + uri);
192 rep = newFixedLengthResponse(
193 Status.INTERNAL_ERROR,
194 NanoHTTPD.MIME_PLAINTEXT,
195 "An error happened");
196 }
197 } else {
198 if (uri.startsWith("/"))
199 uri = uri.substring(1);
200 InputStream in = IOUtils.openResource(
201 WebLibraryServerIndex.class, uri);
202 if (in != null) {
203 String mimeType = MIME_PLAINTEXT;
204 if (uri.endsWith(".css")) {
205 mimeType = "text/css";
206 } else if (uri.endsWith(".html")) {
207 mimeType = "text/html";
208 } else if (uri.endsWith(".js")) {
209 mimeType = "text/javascript";
210 }
211 rep = newChunkedResponse(Status.OK, mimeType,
212 in);
213 }
214
215 if (rep == null) {
216 getTraceHandler().trace("404: " + uri);
217 rep = newFixedLengthResponse(Status.NOT_FOUND,
218 NanoHTTPD.MIME_PLAINTEXT, "Not Found");
219 }
220 }
221 } catch (Exception e) {
222 Instance.getInstance().getTraceHandler().error(
223 new IOException("Cannot process web request",
224 e));
225 rep = newFixedLengthResponse(Status.INTERNAL_ERROR,
226 NanoHTTPD.MIME_PLAINTEXT, "An error occured");
227 }
228 }
229
230 return rep;
231 }
232 };
233
6673ec59
NR
234 if (ssf != null)
235
236 {
33d40b9f
NR
237 getTraceHandler().trace("Install SSL on the web server...");
238 server.makeSecure(ssf, null);
239 getTraceHandler().trace("Done.");
240 }
241 }
242
243 @Override
244 public void run() {
245 try {
246 server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
247 } catch (IOException e) {
248 tracer.error(new IOException("Cannot start the web server", e));
249 }
250 }
251
252 /**
253 * The traces handler for this {@link WebLibraryServerHtml}.
254 *
255 * @return the traces handler
256 */
257 public TraceHandler getTraceHandler() {
258 return tracer;
259 }
260
261 /**
262 * The traces handler for this {@link WebLibraryServerHtml}.
263 *
264 * @param tracer
265 * the new traces handler
266 */
267 public void setTraceHandler(TraceHandler tracer) {
268 if (tracer == null) {
269 tracer = new TraceHandler(false, false, false);
270 }
271
272 this.tracer = tracer;
273 }
274
275 private Response loginPage(WLoginResult login, String uri) {
276 StringBuilder builder = new StringBuilder();
277
278 appendPreHtml(builder, true);
279
280 if (login.isBadLogin()) {
281 builder.append("<div class='error'>Bad login or password</div>");
282 } else if (login.isBadCookie()) {
283 builder.append("<div class='error'>Your session timed out</div>");
284 }
285
286 if (WebLibraryUrls.LOGOUT_URL.equals(uri)) {
287 uri = WebLibraryUrls.INDEX_URL;
288 }
289
290 builder.append(
291 "<form method='POST' action='" + uri + "' class='login'>\n");
292 builder.append(
293 "<p>You must be logged into the system to see the stories.</p>");
294 builder.append("\t<input type='text' name='login' />\n");
295 builder.append("\t<input type='password' name='password' />\n");
296 builder.append("\t<input type='submit' value='Login' />\n");
297 builder.append("</form>\n");
298
299 appendPostHtml(builder);
300
301 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
302 NanoHTTPD.MIME_HTML, builder.toString());
303 }
304
305 private Response root(IHTTPSession session, Map<String, String> cookies,
306 WLoginResult login) throws IOException {
307 BasicLibrary lib = Instance.getInstance().getLibrary();
308 MetaResultList result = new MetaResultList(metas(login));
309 StringBuilder builder = new StringBuilder();
310
311 appendPreHtml(builder, true);
312
313 Map<String, String> params = session.getParms();
314
315 String filter = cookies.get("filter");
316 if (params.get("optionNo") != null)
317 filter = null;
318 if (filter == null) {
319 filter = "";
320 }
321
322 String browser = params.get("browser") == null ? ""
323 : params.get("browser");
324 String browser2 = params.get("browser2") == null ? ""
325 : params.get("browser2");
326 String browser3 = params.get("browser3") == null ? ""
327 : params.get("browser3");
328
329 String filterSource = null;
330 String filterAuthor = null;
331 String filterTag = null;
332
333 // TODO: javascript in realtime, using visible=false + hide [submit]
334
335 builder.append("<form class='browser'>\n");
336 builder.append("<div class='breadcrumbs'>\n");
337
338 builder.append("\t<select name='browser'>");
339 appendOption(builder, 2, "", "", browser);
340 appendOption(builder, 2, "Sources", "sources", browser);
341 appendOption(builder, 2, "Authors", "authors", browser);
342 appendOption(builder, 2, "Tags", "tags", browser);
343 builder.append("\t</select>\n");
344
345 if (!browser.isEmpty()) {
346 builder.append("\t<select name='browser2'>");
347 if (browser.equals("sources")) {
348 filterSource = browser2.isEmpty() ? filterSource : browser2;
349 // TODO: if 1 group -> no group
350 appendOption(builder, 2, "", "", browser2);
351 Map<String, List<String>> sources = result.getSourcesGrouped();
352 for (String source : sources.keySet()) {
353 appendOption(builder, 2, source, source, browser2);
354 }
355 } else if (browser.equals("authors")) {
356 filterAuthor = browser2.isEmpty() ? filterAuthor : browser2;
357 // TODO: if 1 group -> no group
358 appendOption(builder, 2, "", "", browser2);
359 Map<String, List<String>> authors = result.getAuthorsGrouped();
360 for (String author : authors.keySet()) {
361 appendOption(builder, 2, author, author, browser2);
362 }
363 } else if (browser.equals("tags")) {
364 filterTag = browser2.isEmpty() ? filterTag : browser2;
365 appendOption(builder, 2, "", "", browser2);
366 for (String tag : result.getTags()) {
367 appendOption(builder, 2, tag, tag, browser2);
368 }
369 }
370 builder.append("\t</select>\n");
371 }
372
373 if (!browser2.isEmpty()) {
374 if (browser.equals("sources")) {
375 filterSource = browser3.isEmpty() ? filterSource : browser3;
376 Map<String, List<String>> sourcesGrouped = result
377 .getSourcesGrouped();
378 List<String> sources = sourcesGrouped.get(browser2);
379 if (sources != null && !sources.isEmpty()) {
380 // TODO: single empty value
381 builder.append("\t<select name='browser3'>");
382 appendOption(builder, 2, "", "", browser3);
383 for (String source : sources) {
384 appendOption(builder, 2, source, source, browser3);
385 }
386 builder.append("\t</select>\n");
387 }
388 } else if (browser.equals("authors")) {
389 filterAuthor = browser3.isEmpty() ? filterAuthor : browser3;
390 Map<String, List<String>> authorsGrouped = result
391 .getAuthorsGrouped();
392 List<String> authors = authorsGrouped.get(browser2);
393 if (authors != null && !authors.isEmpty()) {
394 // TODO: single empty value
395 builder.append("\t<select name='browser3'>");
396 appendOption(builder, 2, "", "", browser3);
397 for (String author : authors) {
398 appendOption(builder, 2, author, author, browser3);
399 }
400 builder.append("\t</select>\n");
401 }
402 }
403 }
404
405 builder.append("\t<input type='submit' value='Select'/>\n");
406 builder.append("</div>\n");
407
408 // TODO: javascript in realtime, using visible=false + hide [submit]
409 builder.append("<div class='filter'>\n");
410 builder.append("\t<span class='label'>Filter: </span>\n");
411 builder.append(
412 "\t<input name='optionName' type='hidden' value='filter' />\n");
413 builder.append("\t<input name='optionValue' type='text' value='"
414 + filter + "' place-holder='...' />\n");
415 builder.append("\t<input name='optionNo' type='submit' value='x' />");
416 builder.append(
417 "\t<input name='submit' type='submit' value='Filter' />\n");
418 builder.append("</div>\n");
419 builder.append("</form>\n");
420
421 builder.append("\t<div class='books'>");
422 for (MetaData meta : result.getMetas()) {
423 if (!filter.isEmpty() && !meta.getTitle().toLowerCase()
424 .contains(filter.toLowerCase())) {
425 continue;
426 }
427
428 // TODO Sub sources
429 if (filterSource != null
430 && !filterSource.equals(meta.getSource())) {
431 continue;
432 }
433
434 // TODO: sub authors
435 if (filterAuthor != null
436 && !filterAuthor.equals(meta.getAuthor())) {
437 continue;
438 }
439
440 if (filterTag != null && !meta.getTags().contains(filterTag)) {
441 continue;
442 }
443
444 builder.append("<div class='book_line'>");
445 builder.append("<a href='");
446 builder.append(
447 WebLibraryUrls.getViewUrl(meta.getLuid(), null, null));
448 builder.append("'");
449 builder.append(" class='link'>");
450
451 if (lib.isCached(meta.getLuid())) {
452 // â—‰ = &#9673;
453 builder.append(
454 "<span class='cache_icon cached'>&#9673;</span>");
455 } else {
456 // â—‹ = &#9675;
457 builder.append(
458 "<span class='cache_icon uncached'>&#9675;</span>");
459 }
460 builder.append("<span class='luid'>");
461 builder.append(meta.getLuid());
462 builder.append("</span>");
463 builder.append("<span class='title'>");
464 builder.append(meta.getTitle());
465 builder.append("</span>");
466 builder.append("<span class='author'>");
467 if (meta.getAuthor() != null && !meta.getAuthor().isEmpty()) {
468 builder.append("(").append(meta.getAuthor()).append(")");
469 }
470 builder.append("</span>");
471 builder.append("</a></div>\n");
472 }
473 builder.append("</div>");
474
475 appendPostHtml(builder);
476 return NanoHTTPD.newFixedLengthResponse(builder.toString());
477 }
478
479 private Response getViewer(Map<String, String> cookies, String uri,
480 WLoginResult login) {
481 String[] cover = uri.split("/");
482 int off = 2;
483
484 if (cover.length < off + 2) {
485 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
486 NanoHTTPD.MIME_PLAINTEXT, null);
487 }
488
489 String type = cover[off + 0];
490 String luid = cover[off + 1];
491 String chapterStr = cover.length < off + 3 ? null : cover[off + 2];
492 String paragraphStr = cover.length < off + 4 ? null : cover[off + 3];
493
494 // 1-based (0 = desc)
495 int chapter = 0;
496 if (chapterStr != null) {
497 try {
498 chapter = Integer.parseInt(chapterStr);
499 if (chapter < 0) {
500 throw new NumberFormatException();
501 }
502 } catch (NumberFormatException e) {
503 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
504 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
505 }
506 }
507
508 // 1-based
509 int paragraph = 0;
510 if (paragraphStr != null) {
511 try {
512 paragraph = Integer.parseInt(paragraphStr);
513 if (paragraph <= 0) {
514 throw new NumberFormatException();
515 }
516 } catch (NumberFormatException e) {
517 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
518 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
519 }
520 }
521
522 try {
523 Story story = story(luid, login);
524 if (story == null) {
525 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
526 NanoHTTPD.MIME_PLAINTEXT, "Story not found");
527 }
528
529 StringBuilder builder = new StringBuilder();
530 appendPreHtml(builder, false);
531
532 // For images documents, always go to the images if not chap 0 desc
533 if (story.getMeta().isImageDocument()) {
534 if (chapter > 0 && paragraph <= 0)
535 paragraph = 1;
536 }
537
538 Chapter chap = null;
539 if (chapter <= 0) {
540 chap = story.getMeta().getResume();
541 } else {
542 try {
543 chap = story.getChapters().get(chapter - 1);
544 } catch (IndexOutOfBoundsException e) {
545 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
546 NanoHTTPD.MIME_PLAINTEXT, "Chapter not found");
547 }
548 }
549
550 String first, previous, next, last;
551
552 StringBuilder content = new StringBuilder();
553
554 String disabledLeft = "";
555 String disabledRight = "";
556 String disabledZoomReal = "";
557 String disabledZoomWidth = "";
558 String disabledZoomHeight = "";
559
560 if (paragraph <= 0) {
561 first = WebLibraryUrls.getViewUrl(luid, 0, null);
562 previous = WebLibraryUrls.getViewUrl(luid,
563 (Math.max(chapter - 1, 0)), null);
564 next = WebLibraryUrls.getViewUrl(luid,
565 (Math.min(chapter + 1, story.getChapters().size())),
566 null);
567 last = WebLibraryUrls.getViewUrl(luid,
568 story.getChapters().size(), null);
569
570 StringBuilder desc = new StringBuilder();
571
572 if (chapter <= 0) {
573 desc.append("<h1 class='title'>");
574 desc.append(story.getMeta().getTitle());
575 desc.append("</h1>\n");
576 desc.append("<div class='desc'>\n");
577 desc.append("\t<a href='" + next + "' class='cover'>\n");
578 desc.append("\t\t<img src='/story/" + luid + "/cover'/>\n");
579 desc.append("\t</a>\n");
580 desc.append("\t<table class='details'>\n");
581 Map<String, String> details = BasicLibrary
582 .getMetaDesc(story.getMeta());
583 for (String key : details.keySet()) {
584 appendTableRow(desc, 2, key, details.get(key));
585 }
586 desc.append("\t</table>\n");
587 desc.append("</div>\n");
588 desc.append("<h1 class='title'>Description</h1>\n");
589 }
590
591 content.append("<div class='viewer text'>\n");
592 content.append(desc);
593 String description = new TextOutput(false).convert(chap,
594 chapter > 0);
595 content.append(chap.getParagraphs().size() <= 0
596 ? "No content provided."
597 : description);
598 content.append("</div>\n");
599
600 if (chapter <= 0)
601 disabledLeft = " disabled='disbaled'";
602 if (chapter >= story.getChapters().size())
603 disabledRight = " disabled='disbaled'";
604 } else {
605 first = WebLibraryUrls.getViewUrl(luid, chapter, 1);
606 previous = WebLibraryUrls.getViewUrl(luid, chapter,
607 (Math.max(paragraph - 1, 1)));
608 next = WebLibraryUrls.getViewUrl(luid, chapter,
609 (Math.min(paragraph + 1, chap.getParagraphs().size())));
610 last = WebLibraryUrls.getViewUrl(luid, chapter,
611 chap.getParagraphs().size());
612
613 if (paragraph <= 1)
614 disabledLeft = " disabled='disbaled'";
615 if (paragraph >= chap.getParagraphs().size())
616 disabledRight = " disabled='disbaled'";
617
618 // First -> previous *chapter*
619 if (chapter > 0)
620 disabledLeft = "";
621 first = WebLibraryUrls.getViewUrl(luid,
622 (Math.max(chapter - 1, 0)), null);
623 if (paragraph <= 1) {
624 previous = first;
625 }
626
627 Paragraph para = null;
628 try {
629 para = chap.getParagraphs().get(paragraph - 1);
630 } catch (IndexOutOfBoundsException e) {
631 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
632 NanoHTTPD.MIME_PLAINTEXT,
633 "Paragraph " + paragraph + " not found");
634 }
635
636 if (para.getType() == ParagraphType.IMAGE) {
637 String zoomStyle = "max-width: 100%;";
638 disabledZoomWidth = " disabled='disabled'";
639 String zoomOption = cookies.get("zoom");
640 if (zoomOption != null && !zoomOption.isEmpty()) {
641 if (zoomOption.equals("real")) {
642 zoomStyle = "";
643 disabledZoomWidth = "";
644 disabledZoomReal = " disabled='disabled'";
645 } else if (zoomOption.equals("width")) {
646 zoomStyle = "max-width: 100%;";
647 } else if (zoomOption.equals("height")) {
648 // see height of navbar + optionbar
649 zoomStyle = "max-height: calc(100% - 128px);";
650 disabledZoomWidth = "";
651 disabledZoomHeight = " disabled='disabled'";
652 }
653 }
654
655 String javascript = "document.getElementById(\"previous\").click(); return false;";
656 content.append(String.format("" //
657 + "<a class='viewer link' oncontextmenu='%s' href='%s'>"
658 + "<img class='viewer img' style='%s' src='%s'/>"
659 + "</a>", //
660 javascript, //
661 next, //
662 zoomStyle, //
663 WebLibraryUrls.getStoryUrl(luid, chapter,
664 paragraph)));
665 } else {
666 content.append(String.format("" //
667 + "<div class='viewer text'>%s</div>", //
668 para.getContent()));
669 }
670 }
671
672 builder.append(String.format("" //
673 + "<div class='bar navbar'>\n" //
674 + "\t<a%s class='button first' href='%s'>&lt;&lt;</a>\n"//
675 + "\t<a%s id='previous' class='button previous' href='%s'>&lt;</a>\n" //
676 + "\t<div class='gotobox itemsbox'>\n" //
677 + "\t\t<div class='button goto'>%d</div>\n" //
678 + "\t\t<div class='items goto'>\n", //
679 disabledLeft, first, //
680 disabledLeft, previous, //
681 paragraph > 0 ? paragraph : chapter //
682 ));
683
684 // List of chap/para links
685
686 appendItemA(builder, 3, WebLibraryUrls.getViewUrl(luid, 0, null),
687 "Description", paragraph == 0 && chapter == 0);
688 if (paragraph > 0) {
689 for (int i = 1; i <= chap.getParagraphs().size(); i++) {
690 appendItemA(builder, 3,
691 WebLibraryUrls.getViewUrl(luid, chapter, i),
692 "Image " + i, paragraph == i);
693 }
694 } else {
695 int i = 1;
696 for (Chapter c : story.getChapters()) {
697 String chapName = "Chapter " + c.getNumber();
698 if (c.getName() != null && !c.getName().isEmpty()) {
699 chapName += ": " + c.getName();
700 }
701
702 appendItemA(builder, 3,
703 WebLibraryUrls.getViewUrl(luid, i, null), chapName,
704 chapter == i);
705
706 i++;
707 }
708 }
709
710 builder.append(String.format("" //
711 + "\t\t</div>\n" //
712 + "\t</div>\n" //
713 + "\t<a%s class='button next' href='%s'>&gt;</a>\n" //
714 + "\t<a%s class='button last' href='%s'>&gt;&gt;</a>\n"//
715 + "</div>\n", //
716 disabledRight, next, //
717 disabledRight, last //
718 ));
719
720 builder.append(content);
721
722 builder.append("<div class='bar optionbar ");
723 if (paragraph > 0) {
724 builder.append("s4");
725 } else {
726 builder.append("s1");
727 }
728 builder.append("'>\n");
729 builder.append(" <a class='button back' href='/'>BACK</a>\n");
730
731 if (paragraph > 0) {
732 builder.append(String.format("" //
733 + "\t<a%s class='button zoomreal' href='%s'>REAL</a>\n"//
734 + "\t<a%s class='button zoomwidth' href='%s'>WIDTH</a>\n"//
735 + "\t<a%s class='button zoomheight' href='%s'>HEIGHT</a>\n"//
736 + "</div>\n", //
737 disabledZoomReal,
738 uri + "?optionName=zoom&optionValue=real", //
739 disabledZoomWidth,
740 uri + "?optionName=zoom&optionValue=width", //
741 disabledZoomHeight,
742 uri + "?optionName=zoom&optionValue=height" //
743 ));
744 }
745
746 appendPostHtml(builder);
747 return NanoHTTPD.newFixedLengthResponse(Status.OK,
748 NanoHTTPD.MIME_HTML, builder.toString());
749 } catch (IOException e) {
750 Instance.getInstance().getTraceHandler()
751 .error(new IOException("Cannot get image: " + uri, e));
752 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
753 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
754 }
755 }
756
757 protected Response newInputStreamResponse(String mimeType, InputStream in) {
758 if (in == null) {
759 return NanoHTTPD.newFixedLengthResponse(Status.NO_CONTENT, "",
760 null);
761 }
762 return NanoHTTPD.newChunkedResponse(Status.OK, mimeType, in);
763 }
764
765 private String getContentOf(String file) {
766 InputStream in = IOUtils.openResource(WebLibraryServerIndex.class,
767 file);
768 if (in != null) {
769 try {
770 return IOUtils.readSmallStream(in);
771 } catch (IOException e) {
772 Instance.getInstance().getTraceHandler().error(
773 new IOException("Cannot get file: index.pre.html", e));
774 }
775 }
776
777 return "";
778 }
779
780 private void appendPreHtml(StringBuilder builder, boolean banner) {
781 String favicon = "favicon.ico";
782 String icon = Instance.getInstance().getUiConfig()
783 .getString(UiConfig.PROGRAM_ICON);
784 if (icon != null) {
785 favicon = "icon_" + icon.replace("-", "_") + ".png";
786 }
787
788 builder.append(
789 getContentOf("index.pre.html").replace("favicon.ico", favicon));
790
791 if (banner) {
792 builder.append("<div class='banner'>\n");
793 builder.append("\t<img class='ico' src='/") //
794 .append(favicon) //
795 .append("'/>\n");
796 builder.append("\t<h1>Fanfix</h1>\n");
797 builder.append("\t<h2>") //
798 .append(Version.getCurrentVersion()) //
799 .append("</h2>\n");
800 builder.append("</div>\n");
801 }
802 }
803
804 private void appendPostHtml(StringBuilder builder) {
805 builder.append(getContentOf("index.post.html"));
806 }
807
808 private void appendOption(StringBuilder builder, int depth, String name,
809 String value, String selected) {
810 for (int i = 0; i < depth; i++) {
811 builder.append("\t");
812 }
813 builder.append("<option value='").append(value).append("'");
814 if (value.equals(selected)) {
815 builder.append(" selected='selected'");
816 }
817 builder.append(">").append(name).append("</option>\n");
818 }
819
820 private void appendTableRow(StringBuilder builder, int depth,
821 String... tds) {
822 for (int i = 0; i < depth; i++) {
823 builder.append("\t");
824 }
825
826 int col = 1;
827 builder.append("<tr>");
828 for (String td : tds) {
829 builder.append("<td class='col");
830 builder.append(col++);
831 builder.append("'>");
832 builder.append(td);
833 builder.append("</td>");
834 }
835 builder.append("</tr>\n");
836 }
837
838 private void appendItemA(StringBuilder builder, int depth, String link,
839 String name, boolean selected) {
840 for (int i = 0; i < depth; i++) {
841 builder.append("\t");
842 }
843
844 builder.append("<a href='");
845 builder.append(link);
846 builder.append("' class='item goto");
847 if (selected) {
848 builder.append(" selected");
849 }
850 builder.append("'>");
851 builder.append(name);
852 builder.append("</a>\n");
853 }
854}